您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
A QoL script for AID, adding customizable hotkeys, also increases performance by removing the countless span elements from last response
// ==UserScript== // @name AIDungeon QoL Tool10 // @version 1.2.0 // @description A QoL script for AID, adding customizable hotkeys, also increases performance by removing the countless span elements from last response // @author randyv // @match https://*.aidungeon.com/* // @icon https://play-lh.googleusercontent.com/ALmVcUVvR8X3q-hOUbcR7S__iicLgIWDwM9K_9PJy87JnK1XfHSi_tp1sUlJJBVsiSc // @require https://code.jquery.com/jquery-3.7.1.min.js // @require https://update.gf.qytechs.cn/scripts/383527/701631/Wait_for_key_elements.js // @require https://update.gf.qytechs.cn/scripts/439099/1203718/MonkeyConfig%20Modern%20Reloaded.js // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @grant GM_registerMenuCommand // @license MIT // @namespace https://gf.qytechs.cn/users/1302066 // ==/UserScript== /* global jQuery, $, waitForKeyElements, MonkeyConfig */ /// require https://cdn.jsdelivr.net/npm/tampermonkey-require-for-react /// @downloadURL https://update.gf.qytechs.cn/scripts/1302066/AIDungeon%20QoL%20Tool.user.js /// @updateURL https://update.gf.qytechs.cn/scripts/1302066/AIDungeon%20QoL%20Tool.meta.js /// require https://cdn.jsdelivr.net/npm/tampermonkey-require-for-react const $ = jQuery.noConflict(true); /******************************** * Code for handling the configuration menu and for handling shortcuts. */ function addEventListeners(element, events, handler) { events.forEach((event) => { if (event.startsWith('touch')) { element.addEventListener(event, handler, { passive: true }); // Mark touch events as passive } else { element.addEventListener(event, handler); // Other events can be added normally } }); } if (0) { function disableCustomContextMenu(button) { console.log("called disableCustomContextMenu"); // Remove existing listeners (optional, but good practice) button.removeEventListener('contextmenu', event => { }); // Add listener using capture phase for higher priority button.addEventListener('contextmenu', (event) => { event.preventDefault(); event.stopPropagation(); // Stop propagation to prevent AIDungeon's listener from triggering }, true); // true for capture phase } // Mutation observer for dynamically added buttons const buttonObserver = new MutationObserver((mutationsList, observer) => { for (const mutation of mutationsList) { if (mutation.type === 'childList') { for (const node of mutation.addedNodes) { if (node.nodeName === 'DIV' && node.matches('[role="button"]')) { disableCustomContextMenu(node); } } } } }); buttonObserver.observe(document.body, { childList: true, subtree: true }); } function waitForSubtreeElements(selector, callback, targetNode, runImmediately = false) { function mutationObserverCallback(mutationsList, observer) { const elements = targetNode.querySelectorAll(selector); if (elements.length > 0) { observer.disconnect(); callback(elements); } } const observer = new MutationObserver(mutationObserverCallback); observer.observe(targetNode, { childList: true, subtree: true }); if (runImmediately) { mutationObserverCallback([], observer); } /* const observer = new MutationObserver((mutationsList, observer) => { const elements = targetNode.querySelectorAll(selector); if (elements.length > 0) { observer.disconnect(); callback(elements); } }); observer.observe(targetNode, { childList: true, subtree: true }); if (runImmediately) { const elements = targetNode.querySelectorAll(selector); if (elements.length > 0) { observer.disconnect(); callback(elements); } } */ } const getSetTextFunc = (value, parent) => { const inputElem = $(parent || value).find('input'); if (!parent) { const booleans = inputElem .filter(':checkbox') .map((_, el) => el.checked) .get(); if (!booleans[0]) return inputElem.val().toUpperCase(); return booleans; } else { inputElem.each((i, el) => { if (el.type === 'checkbox') el.checked = value[i]; else el.value = value.toUpperCase(); }); } }; const dummy = (value, parent) => { }; const cfg = new MonkeyConfig({ title: 'Configure', menuCommand: true, params: { Modifier_Keys: { type: 'custom', 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>', set: getSetTextFunc, get: getSetTextFunc, default: [true, true, false] }, Take_Turn: { type: 'custom', html: '<input type="text" maxlength="1" />', set: getSetTextFunc, get: getSetTextFunc, default: 'C' }, Continue: { type: 'custom', html: '<input type="text" maxlength="1" />', set: getSetTextFunc, get: getSetTextFunc, default: 'A' }, Retry: { type: 'custom', html: '<input type="text" maxlength="1" />', set: getSetTextFunc, get: getSetTextFunc, default: 'S' }, Retry_History: { type: 'custom', html: '<input type="text" maxlength="1" />', set: getSetTextFunc, get: getSetTextFunc, default: 'X' }, Erase: { type: 'custom', html: '<input type="text" maxlength="1" />', set: getSetTextFunc, get: getSetTextFunc, default: 'D' }, Do: { type: 'custom', html: '<input type="text" maxlength="1" />', set: getSetTextFunc, get: getSetTextFunc, default: 'Q' }, Say: { type: 'custom', html: '<input type="text" maxlength="1" />', set: getSetTextFunc, get: getSetTextFunc, default: 'W' }, Story: { type: 'custom', html: '<input type="text" maxlength="1" />', set: getSetTextFunc, get: getSetTextFunc, default: 'E' }, See: { type: 'custom', html: '<input type="text" maxlength="1" />', set: getSetTextFunc, get: getSetTextFunc, default: 'R' }, Response_Underline: { type: 'checkbox', default: true }, Response_Bg_Color: { type: 'checkbox', default: false }, '_label': { type: 'custom', label: '<HR>', set: dummy, get: dummy, html: '<HR>' }, Toggle_Site: { type: 'custom', html: '<input type="text" maxlength="1" />', set: getSetTextFunc, get: getSetTextFunc, default: 'T' }, User_Name: { type: 'custom', html: '<input type="text" maxlength="1" />', set: getSetTextFunc, get: getSetTextFunc, default: 'Z' }, User_Profile: { type: 'custom', html: '<input type="text" maxlength="1" />', set: getSetTextFunc, get: getSetTextFunc, default: 'G' }, Continue_Adventure: { type: 'custom', html: '<input type="text" maxlength="1" />', set: getSetTextFunc, get: getSetTextFunc, default: 'V' }, Flame: { type: 'custom', html: '<input type="text" maxlength="1" />', set: getSetTextFunc, get: getSetTextFunc, default: 'F' }, Modal_Dimensions: { type: 'custom', html: ` <label for="Modal_Width">Width:</label> <input id="Modal_Width" type="number" min="100" style="width: 100px" /> px <label for="Modal_Height">Height:</label> <input id="Modal_Height" type="number" min="100" style="width: 100px" /> px`, set: (values, parent) => { const [width, height] = values.map(Number); // Convert to numbers parent.querySelector('#Modal_Width').value = width; parent.querySelector('#Modal_Height').value = height; }, get: (parent) => { return [parent.querySelector('#Modal_Width').value, parent.querySelector('#Modal_Height').value]; }, default: [512, 512] // Default values for width and height }, Save_Raw_Text: { type: 'checkbox', default: false }, Default_SC_Notes: { type: 'text', default: 'Unused.' }, 'Delimiter Settings': { type: 'custom', html: 'Customize delimiters for Story Card insertion.', set: dummy, get: dummy, default: 'Customize delimiters for Story Card insertion.' }, Delimiter_Start: { type: 'text', default: '{ Story Card: ' }, Delimiter_End: { type: 'text', default: ' }' } } }); const actionArray = [ { name: 'Take_Turn', type: 'Command', 'aria-Label': 'Command: take a turn', active: ["/play"] }, { name: 'Continue', type: 'Command', 'aria-Label': 'Command: continue', active: ["/play"] }, { name: 'Retry', type: 'Command', 'aria-Label': 'Command: retry', active: ["/play"] }, { name: 'Retry_History', type: 'History', 'aria-Label': 'Retry history', active: ["/play"] }, { name: 'Erase', type: 'Command', 'aria-Label': 'Command: erase', active: ["/play"] }, { name: 'Do', type: 'Mode', 'aria-Label': "Set to 'Do' mode", active: ["/play"] }, { name: 'Say', type: 'Mode', 'aria-Label': "Set to 'Say' mode", active: ["/play"] }, { name: 'Story', type: 'Mode', 'aria-Label': "Set to 'Story' mode", active: ["/play"] }, { name: 'See', type: 'Mode', 'aria-Label': "Set to 'See' mode", active: ["/play"] }, { name: 'Flame', type: 'Command', 'aria-Label': 'Game Menu', active: ["/play"] }, { name: 'User_Name', type: 'User_Name', 'aria-Label': 'Game Menu', active: ["/play"] }, { name: 'User_Profile', type: 'User_Profile', 'aria-Label': 'Game Menu', active: ["/play", "/profile/", "/scenario/", "/adventure/"] }, { name: 'Continue_Adventure', type: 'Continue_Adventure', 'aria-Label': 'Play', active: ["/profile/", "/scenario/", "/adventure/"] }, { name: 'Toggle_Site', type: 'Toggle_Site', 'aria-Label': 'Toggle Site', active: ["play.aidungeon.com", "beta.aidungeon.com"] } ]; const actionKeys = actionArray.map((action) => cfg.get(action.name)); // Modified handleKeyPress function const isMac = window.navigator.userAgentData?.platform?.toLowerCase().includes('mac'); const handleKeyPress = (e) => { if (e.repeat) return; const key = e.key.toUpperCase(); //const modifiers = ['ALT', 'CTRL', 'SHIFT'].map((mod) => e[`${mod.toLowerCase()}Key`]); const modifiers = ['ALT', 'CTRL', 'SHIFT'].map((mod) => { // For Mac, use Cmd instead of Ctrl return (mod === 'CTRL' && isMac) ? e.metaKey : e[`${mod.toLowerCase()}Key`]; }); const modifsActive = modifiers.every((value, index) => value === cfg.get('Modifier_Keys')[index]); const index = actionKeys.indexOf(key); if (modifsActive && index !== -1) { const action = actionArray[index]; let isPageActive = false; const fullURL = window.location.href; // Determine if the current page is active based on the action's "active" property if (Array.isArray(action.active)) { // Array of strings: check if pathname includes any of the strings isPageActive = action.active.some(path => fullURL.includes(path)); } else if (action.active instanceof RegExp) { // Regular expression: check if pathname matches the regex isPageActive = action.active.test(fullURL); } else if (typeof action.active === 'function') { // Function: call the function to determine if the page is active isPageActive = action.active(fullURL); } else { console.warn("Invalid 'active' property type for action:", action.name); } if (isPageActive) { e.preventDefault(); e.stopPropagation(); const targetElem = `[aria-label="${action['aria-Label']}"]`; if ($("[aria-label='Close text input']").length) $("[aria-label='Close text input']").click(); if (action.type === 'Command') setTimeout(() => $(targetElem).click(), 50); else if (action.type === 'Mode') delayedClicks([() => $('[aria-label="Command: take a turn"]').click(), () => $('[aria-label="Change input mode"]').click(), () => $(targetElem).click()]); else if (action.type === 'History' && $('[aria-label="Retry history"]').length) setTimeout(() => $(targetElem).click(), 50); else if (action.type === 'User_Profile') { if (window.location.pathname.includes('/play')) { delayedClicks([ () => $('[role="button"][aria-label="Game Menu"]').click(), () => $('[role="button"][aria-label^="View"][aria-label$="profile"]').click() ]); } else { delayedClicks([ () => $('[role="button"][aria-label="User Menu"]').click(), () => $('[role="button"][aria-label="My Stuff/Profile"]').click() ]); } } else if (action.type === 'Continue_Adventure') { console.log("Got continue Adventure"); delayedClicks([ () => $('[role="button"][aria-label="Play"]').click(), () => $('[role="button"][aria-label="Continue Adventure"]').click() ]); } else if (action.type === 'User_Name') { document.addEventListener('forceFocus', (event) => { //.log("Got custome forced event."); event.target.focus(); // Focus on the target of the event (the input field) }); delayedClicks([ () => $('[role="button"][aria-label="Game Menu"]').click(), () => $('[role="button"][aria-label="Open player menu"]').click(), () => $('[role="button"][aria-label="Edit Character Name"]').click(), /* , () => { const playersGroup = $('[role="group"][aria-label="Players"]'); //const viewProfileButton = $('[role="group"][aria-label="Players"]'); const inputField = playersGroup.find('button[aria-label^="View"][aria-label$="profile"]').next().find('input')[0]; //const playersGroup = $('[role="group"][aria-label="Players"]'); //const viewProfileButton = playersGroup.querySelector('button[aria-label^="View"][aria-label$="profile"]'); //const inputField = viewProfileButton?.nextSibling?.firstChild; // Path to input from profile button. if (inputField) { inputField.id = "flameplayername"; // This works, can see in dev console. const focusElement = inputField.parentElement; setTimeout( () => { //const inputField = document.querySelector('input#flameplayername'); focusElement.dispatchEvent(new CustomEvent('forceFocus', { bubbles: true })); //const inputField = document.querySelector('input#flameplayername'); //focusElement.dispatchEvent(new CustomEvent('forceFocus', { bubbles: true })); //focusElement.click(); // Doesn't work. //focusElement.focus(); // Doesn't work. //focusElement.trigger('click'); // Doesn't work. //focusElement.dispatchEvent(new Event('focus', { bubbles: true })); // Doesn't work. //focusElement.dispatchEvent(new MouseEvent('click', { bubbles: true })); // Doesn't work. const mouseDownEvent = new MouseEvent('mousedown', { bubbles: true }); // Doesn't work. const mouseUpEvent = new MouseEvent('mouseup', { bubbles: true }); // Doesn't work. focusElement.dispatchEvent(mouseDownEvent); // Doesn't work. focusElement.dispatchEvent(mouseUpEvent); // Doesn't work. if (0) { // This doesn't work. const existingClickListener = inputField.onclick; // Get the existing click handler focusElement.onclick = null; // Remove it focusElement.click(); // Or use dispatchEvent as shown above focusElement.onclick = existingClickListener; } }, 2000 ); } }, */ () => $('input#flameplayername').click(), // Doesn't work. () => $('input#flameplayername').trigger('click'), // Doesn't work. () => $('input#flameplayername').focus() // Doesn't work. ] ); } else if (action.type === 'Toggle_Site') { const currentURL = window.location.href; console.log("Got Site Toggle: ", currentURL); const betaSite = 'beta.aidungeon.com'; const playSite = 'play.aidungeon.com'; const newURL = currentURL.includes(betaSite) ? currentURL.replace(betaSite, playSite) : currentURL.replace(playSite, betaSite); console.log("Got Site Toggle: ", newURL); window.location.href = newURL; } // End action.type } // End isPageActive } const selectKeys = ['ARROWLEFT', 'ENTER', 'ARROWRIGHT']; if (selectKeys.includes(key) && $('[role="dialog"]').length) setTimeout(() => $("[role='dialog']").find("[role='button']")[selectKeys.indexOf(key)].click(), 50); }; const delayedClicks = (clicks, i = 0) => { if (i < clicks.length) { setTimeout(() => { clicks[i](); delayedClicks(clicks, i + 1); }, 50); } }; class DOMObserver { constructor(callback, targetNode, options, startImmediately = false) { this.observer = new MutationObserver(callback); this.targetNode = targetNode; this.options = options; if (startImmediately) { this.observe(); } } destroy() { this.disconnect(); this.observer = null; this.targetNode = null; this.options = null; } observe(targetNode = this.targetNode, options = this.options) { if (this.observer && targetNode && targetNode.nodeType === Node.ELEMENT_NODE) { // Ensure targetNode is an Element this.observer.observe(targetNode, options); } else { console.warn("Target node is not a valid element:", targetNode); // For debugging } } disconnect() { if (this.observer !== null) { this.observer.disconnect(); } } takeRecords() { return this.observer ? this.observer.takeRecords() : []; // Return empty array if observer is null } get isConnected() { return this.observer && this.observer.isConnected(); // Check if observer exists and is connected } } GM_addStyle(` .css-11aywtz,._dsp_contents { user-select: text !important; } `); //textarea:not(#game-text-input, #transition-opacity, #shadow-box, #do-not-copy) { GM_addStyle(` /* ._pt-1316335136 { padding-top: 6px !important; } ._pb-1316335136 { padding-bottom: 6px !important; } */ /* ._pr-1481558400 { padding-right: 0px !important; } ._pl-1481558338 { padding-left: 8px !important; } ._pr-1481558338 { padding-right: 8px !important; } ._pt-1316335167 { padding-top: 8px !important; } ._pb-1316335167 { padding-bottom: 8px !important; } ._pl-1316335167 { padding-left: 8px !important; } ._pr-1316335167 { padding-right: 8px !important; } ._pl-1481558307 { padding-left: 8px !important; } ._pr-1481558307 { padding-right: 8px !important; } */ /* Target: every storyCardsTab list of button type with pad or margin left or right */ div#modalInnerContent_storyCardsTab div[role="button"]._pl-1481558338 { padding-left: 0px !important; } div#modalInnerContent_storyCardsTab div[role="button"]._pr-1481558338 { padding-right: 0px !important; } div#modalInnerContent_storyCardsTab div[role="button"]._mr-1481558369 { margin: 0px !important; } div#modalInnerContent_storyCardsTab > div > div { padding-left: 0px !important; padding-right: 0px !important; margin-left: 0px !important; margin-right: 0px !important; } div#modalInnerContent_storyCardsTab > div > div > div > div:nth-child(3) { width: 100% !important; } div#modalInnerContent_storyCardsTab > div > div > div > div:nth-child(3) > * { width: 100% !important; } div#modalInnerContent_storyCardsTab > div > div > div > div:nth-child(5) { padding-bottom: 0px !important; } /* Make Modal bottom right border square for the resize icon. */ div:has([aria-label="Modal" i]) { border-bottom-right-radius: 0px !important; } /* These classes must be overridden to get the square corner. */ ._bbrr-1307609874 { border-bottom-right-radius: 0px !important; } ._bbrr-1881205710 { border-bottom-right-radius: 0px !important; } /* Tweak the padding for modals. */ div[id^="modalHeader_" i] { padding: 8px !important; flex-grow: 0 !important; /* overflow: hidden hidden !important; */ } div[id^="modalContent_" i] { max-height: 100% !important; margin: 0px !important; padding-bottom: 8px !important; padding-top: 8px !important; padding-left: 8px !important; padding-right: 8px !important; scrollbar-gutter: stable !important; min-height: 0px !important; overflow-y: auto !important; overflow-x: hidden !important; } div[id^="modalContent_" i] p[role="heading"] { padding-left: 0px !important; padding-right: 0px !important; margin-left: 0px !important; margin-right: 0px !important; } div[id^="modalInnerContent_" i] button[type="button"] { padding-left: 8px !important; padding-right: 8px !important; margin-left: 0px !important; margin-right: 0px !important; } /* [id^="modalInnerContent_" i] div.is_Column { padding-left: 0px !important; padding-right: 0px !important; margin-left: 0px !important; margin-right: 0px !important; } div[id^="modalContent_" i] + div:has(p[role="heading"]) { padding-left: 0px !important; padding-right: 0px !important; margin-left: 0px !important; margin-right: 0px !important; } div:has([id^="modalContent_" i] p[role="heading" i]) { padding-left: 0px !important; padding-right: 0px !important; margin-left: 0px !important; margin-right: 0px !important; } [id^="modalContent_"] > div, [id^="modalContent_"]._pb-1481558400 { padding: 8px !important; padding-bottom: 8px !important; padding-top: 8px !important; padding-left: 8px !important; padding-right: 8px !important; } */ div[id^="modalContent_" i] + div.has(p[role="heading"]) { padding-left: 0px !important; padding-right: 0px !important; margin-left: 0px !important; margin-right: 0px !important; } div[id^="modalContent_" i] input { padding-left: 8px !important; padding-right: 8px !important; margin-left: 0px !important; margin-right: 0px !important; } div[id^="modalContent_" i] textarea { padding-left: 8px !important; padding-right: 8px !important; margin-left: 0px !important; margin-right: 0px !important; } div[id^="modalInnerContent_" i] { max-height: 100% !important; padding: 0px !important; margin: 0px !important; min-height: 0px !important; overflow-x: unset !important; overflow-y: unset !important; max-height: 100% !important; max-width: 100% !important; } /* [id^="modalInnerContent_"] > div > div > div { padding-left: 0px !important; padding-right: 0px !important; margin: 0px !important; } */ /* Target the specific modal with the selected "Story Cards" tab div[aria-label="Modal"] div[id^="modalHeader_"] div[role="tablist"][aria-label="Section Tabs"] div[role="tab"][aria-label^="Selected tab story cards" i] ~ div[id^="modelContent_"] [id^="modalInnerContent_"] > div > div > div { padding-left: 0px !important; padding-right: 0px !important; margin: 0px !important; } */ /* #modalInnerContent_1722033353146 > div > div > div > div > div:nth-child(3) > div:nth-child(7) > div.css-175oi2r > div [id^="modalInnerContent_"] > div > div > div { [id^="modalHeader_"] div[role="tablist"][aria-label="Section Tabs"] div[role="tab"][aria-label^="Selected tab story cards" i] { [id^="modalInnerContent_"] > div > div > div { padding-left: 0px !important; padding-right: 0px !important; margin: 0px !important; } */ /* > div > div:nth-child(1) > div > div > div > span > div [id^="modalInnerContent_"] > div { padding: 0px !important; padding-bottom: 0px !important; } [id^="modalInnerContent_"] > div > div > div > div > div { padding: 0px !important; margin: 0px !important; } */ /* ._mah-1611765696 { max-height: unset !important; } ._mih-1611762875 { min-height: 200px !important; } input._h-606181821 { height: var(--size-6) !important; } input._gap-1481558338 { gap: var(--space-1) !important; } */ /* max-height: none !important; /* auto does not work, shows error. */ /*max-height: 1024px !important; /* auto does not work, shows error. */ /* min-height: auto !important; /* Or min-height: 0; */ /* height: unset !important; /* This seems to set the height to a value that that's not resizable. */ /* height: initial !important; /* This seems to set the height to a smaller value that that's not resizable. */ /* height: 300px !important; /* This seems to set the height to a value that that's not resizable. */ /* height: 100% !important; /* This seems seems to set it 100% the size of the text area, but it isn't resizable. */ /* height: 25% !important; /* This seems seems to set the internal textarea scrollable region to 25% of the size of the text area, but it isn't resizable. */ /* height: '' !important; /* Makes the text area resizable, but shows as error, and it's the full size of the text. */ /* height: 400px !important; /* Makes the text area resizable, but shows as error, and it's the full size of the text. */ /* Chrome/Opera Fatten up the scroll bar a bit. This also fixes textarea resize icon. */ ::-webkit-scrollbar { width: 8px !important; } /* Put vertical resizers on all textareas. */ textarea:not([aria-label="Text input field" i], #game-text-input, #shadow-box) { min-height: 50px !important; /* Or min-height: 0; */ resize: vertical !important; overflow-y: auto !important; scrollbar-gutter: stable !important; /* This doesn't work the class is overriding it. */ border-bottom-right-radius: 0px !important; /* None of these appear to do anything in chrome (maybe for Mozilla): */ --scrollbar-width: 8px !important; color-scheme: dark !important; --vh: 11.76px !important; } /* Experiments with offing the dimming gradient. .game-text-mask { transition: mask-position .3s ease, -webkit-mask-position .3s ease !important; -webkit-mask-image: linear-gradient(rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.5) 100%) !important; mask-image: linear-gradient(rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.5) 100%) !important; -webkit-mask-size: 100% 100% !important; mask-size: 100% 100% !important; } .game-text-mask { transition: mask-position .3s ease, mask-size .3s ease; -webkit-mask-image: linear-gradient(transparent, rgba(0, 0, 0, 0) 10%, #000 20%, #000); mask-image: linear-gradient(transparent, rgba(0, 0, 0, 0) 10%, #000 20%, #000); -webkit-mask-size: 100% 100%; mask-size: 100% 100%; } */ `); // Clean up the the prompt area to make more efficient. // This is the original code from QoL tool by AliH2K function handleChanges(mutationsList, observer) { for (const mutation of mutationsList) { if (!window.location.href.includes('/play')) { return; // Exit early if not on a Play page } const targetNode = mutation.target; // Get the target node from the mutation let lastResponse = targetNode?.lastChild?.lastChild; if (lastResponse) { // Check if the last child exists, is a span, and if it has children if (lastResponse.lastChild && lastResponse.children.length > 0 && lastResponse.tagName === 'SPAN') { // Check if the last child is an HTMLElement before accessing style if (lastResponse.lastChild instanceof HTMLElement) { lastResponse.lastChild.style.pointerEvents = 'none'; // Set pointerEvents to none } else { console.warn("lastResponse.lastChild is not an HTMLElement:", lastResponse.lastChild); } } else if (lastResponse.lastChild instanceof HTMLElement && lastResponse.lastChild.style.pointerEvents === 'none') { lastResponse.lastChild.style.pointerEvents = ''; // Reset pointerEvents if it was set to none } else { // Handle the case where lastResponse doesn't have a lastChild yet const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { handleChanges(mutations, observer); // Recursively call handleChanges for the new nodes observer.disconnect(); // Stop observing after the child is added break; } } }); observer.observe(lastResponse, { childList: true }); // Observe for changes in childList } // Check if the first child exists, is not a text node, and the parent is a span if (lastResponse.firstChild && lastResponse.firstChild.nodeType !== 3 && lastResponse.tagName === 'SPAN') { //if (lastResponse.firstChild.nodeType !== 3 && lastResponse.tagName === 'SPAN') { const interval = setInterval(() => { const opacity = lastResponse.lastChild instanceof HTMLElement ? getComputedStyle(lastResponse.lastChild).opacity : '1'; if (opacity === '1') { clearInterval(interval); const SPANS = Array.from(lastResponse.children); let joinedText = ''; SPANS.forEach((span) => (joinedText += span.textContent)); while (lastResponse.firstChild && lastResponse.firstChild.nodeType !== 3) lastResponse.removeChild(lastResponse.firstChild); if (joinedText.length > 1) lastResponse.textContent = joinedText; } }, 500); } } } // // Apply Custom CSS // const customCSS = cfg.get('Custom_CSS'); // if (customCSS) { // GM_addStyle(customCSS); // } } //setNativeValue(input, 'foo'); //input.dispatchEvent(new Event('input', { bubbles: true })); function setNativeValue(element, value) { const valueSetter = Object.getOwnPropertyDescriptor(element, 'value').set; const prototype = Object.getPrototypeOf(element); const prototypeValueSetter = Object.getOwnPropertyDescriptor(prototype, 'value').set; if (valueSetter && valueSetter !== prototypeValueSetter) { prototypeValueSetter.call(element, value); } else { valueSetter.call(element, value); } } const ActionToggleMsgOn = "A+"; const ActionToggleMsgOff = "A-"; let actionsExpanded = null; let toggleButtonText = null; GM_addStyle(` [aria-label="Story"] .is_Row { visibility: visible; } .actions-hidden .is_Row > * { visibility: hidden; display: none !important; } .actions-hidden .is_Row { visibility: hidden; margin-top: 2.5em !important; margin-right: 0 !important; margin-bottom: 0 !important; margin-left: 0 !important; padding: 0 !important; } `); /* */ function setActionVisibility(visible) { const container = $("[aria-label='Story']"); if (visible) { container.removeClass("actions-hidden"); } else { container.addClass("actions-hidden"); } actionsExpanded = visible; sessionStorage.setItem("actionsExpanded", actionsExpanded); // Save state } function toggleOnClick(buttonTextElement) { actionsExpanded = !actionsExpanded; setActionVisibility(actionsExpanded); // Use the set function buttonTextElement.innerText = actionsExpanded ? ActionToggleMsgOn : ActionToggleMsgOff; } function removeHeightClasses(element) { const classList = element.classList; // Get the element's classList for (const className of classList) { if (className.startsWith('_mih-') || className.startsWith('_mah-')) { classList.remove(className); } } } function buttonClone(cloneReference, label, action) { if (!cloneReference) { console.warn("Null cloneReference in buttonClone!"); return; } //console.log("cloneReference: ", cloneReference); const clonedElement = cloneReference.cloneNode(true); // Clone the entire reference // Determine if the cloned element is a span or a div (button) const isSpan = clonedElement.tagName === 'SPAN'; // The play page wraps the button DIVs in spans, while the read page does not. const button = isSpan ? clonedElement.querySelector('[role=button]') : clonedElement; // Generate a unique ID const uniqueId = `custom-button-${label.replace(/\s+/g, '-').toLowerCase()}`; button.id = uniqueId; // Remove anything that might disable it. button.classList.remove('disabled'); button.removeAttribute('aria-disabled'); button.setAttribute('style', 'pointer-events: all !important; z-index:100000; font-weight: bold;'); // Explicitly add the 'enabled' attribute to the cloned button button.setAttribute('aria-enabled', 'true'); button.setAttribute('aria-label', `${label} button`); // Add a unique class to the cloned button for easier targeting const uniqueClass = `custom-button-${uniqueId}`; button.classList.add(uniqueClass); //Use GM_addStyle with a more specific selector and !important GM_addStyle(` #${uniqueId}.${uniqueClass} { background-color: black !important; color: white !important; opacity: 1 !important; font-weight: bold !important; } `); // Cache the <p> element for later access const buttonTextElement = button.querySelector('p'); //console.log("button: ", button); buttonTextElement.innerText = label; // Remove the dimming opacity class. buttonTextElement.classList.remove('_o-0d0t546'); button.onclick = (e) => { e.preventDefault(); e.bubbles = false; action(buttonTextElement); // Pass the <p> element to the action function }; return clonedElement; } function headerInject(container, cloneReference, label, action) { const clonedElement = buttonClone(cloneReference, label, action); // Clone the entire reference container.prepend(clonedElement); } /********************************** ** Code for Read Pages. */ function handleReadPage(targetNode) { // Use the second button if available, otherwise use the first // Find all buttons with innerText 'Aa' const aaButtons = [...$('[role=button]')].filter((e) => e.innerText === 'Aa'); const aaButton = aaButtons.length >= 2 ? aaButtons[1] : aaButtons[0]; const buttonContainer = aaButton.parentElement; function onSave(type) { const story = $('[aria-label="Story"]')[0]; const title = $('[role=heading]')[0]?.innerText; const saveRaw = cfg.get('Save_Raw_Text'); if (!story || !title) return alert('Wait for content to load first!'); let text = story.innerText.replaceAll(/w_\w+\n+\s+/g, type === 'text' ? '' : '> '); if (type === 'md') text = '## ' + title + '\n\n' + text; text = text.replaceAll(/\n+/g, '\n\n'); const blob = URL.createObjectURL(new Blob([text], { type: type === 'text' ? 'text/plain' : 'text/x-markdown' })); const a = document.createElement('a'); a.download = title + (type === 'text' ? '.txt' : '.md'); a.href = blob; a.click(); URL.revokeObjectURL(blob); } headerInject(buttonContainer, aaButton, '.txt', () => onSave('text')); headerInject(buttonContainer, aaButton, '.md', () => onSave('md')); headerInject(buttonContainer, aaButton, toggleButtonText, toggleOnClick); } /********************************** ** Code for Modals. */ function insertAfter(referenceNode, newNode) { referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling); } function cloneAndModifyModalButton(modalNode, originalButtonSelector, newButtonText, newButtonIcon) { const originalButton = modalNode.querySelector(originalButtonSelector); if (!originalButton) { console.warn("Original button not found:", originalButtonSelector); return null; } const clonedButton = originalButton.cloneNode(true); // Modify the cloned button's attributes and content clonedButton.setAttribute('aria-label', newButtonText); const buttonTextElement = clonedButton.querySelector('.is_ButtonText'); if (buttonTextElement) { buttonTextElement.textContent = newButtonText; } const iconElement = clonedButton.querySelector('.font_icons'); if (iconElement) { iconElement.textContent = newButtonIcon; } // Remove the original button's click event handler clonedButton.onclick = null; clonedButton.removeEventListener('click', originalButton.onclick); return clonedButton; } // Function to create and insert a button and text fields into the Story Card Modal // For adding { Story Card: } into the entry field. function modifyStoryCardEditor(modalNode) { const mkycfg_startDelim = cfg.get('Delimiter_Start'); const mkycfg_endDelim = cfg.get('Delimiter_End'); const entryField = modalNode.querySelector("textarea[aria-labelledby='scEntryLabel']"); const entryLabel = modalNode.querySelector("p#scEntryLabel"); const entrySection = entryLabel.parentNode; const delimEntryButton = cloneAndModifyModalButton(modalNode, "div[role='button'][aria-label='Close modal']", "Insert", "w_add"); if (!delimEntryButton) return; // Handle the case where the button wasn't found const buttonText = delimEntryButton.querySelector('.is_ButtonText'); delimEntryButton.id = "scDelimInsertButton"; //delimEntryButton.classList.add('is_Button', 'is_ButtonText', 'insert-button', 'css-11aywtz', 'r-6taxm2'); // Add classes for styling // Clone the Triggers section (get parentNode of p with id 'scTriggersLabel') const triggersSection = modalNode.querySelector("p#scTriggersLabel").parentNode; const delimEntrySection = triggersSection.cloneNode(true); // Modify label, placeholder, and aria-label const delimEntryLabel = delimEntrySection.querySelector("p"); delimEntryLabel.setAttribute('aria-label', "Delimiter Entry Label"); delimEntryLabel.id = "scDelimEntryLabel"; delimEntryLabel.textContent = "DELIM ENTRY"; const delimStartInput = delimEntrySection.querySelector("input"); delimStartInput.placeholder = mkycfg_startDelim; delimStartInput.value = mkycfg_startDelim; delimStartInput.setAttribute('aria-label', "Delimiter Start Input"); delimStartInput.setAttribute('aria-labelledby', "scDelimEntryLabel"); delimStartInput.id = "scDelimStartInput"; // Get the span that contains the input field const delimInputSpan = delimEntrySection.querySelector('span._dsp_contents'); delimInputSpan.setAttribute('aria-label', "Delimiter Input Span"); delimInputSpan.id = "scDelimInputSpan"; delimInputSpan.setAttribute('aria-labelledby', "scDelimEntryLabel"); const delimEndInput = delimStartInput.cloneNode(true); delimEndInput.placeholder = mkycfg_endDelim; // Update the placeholder text delimEndInput.value = mkycfg_endDelim; // Update the value text delimEndInput.setAttribute('aria-label', "Delimiter End Input"); delimEndInput.setAttribute('aria-labelledby', "scDelimEntryLabel"); delimEndInput.id = "scDelimEndInput"; delimInputSpan.appendChild(delimEndInput); delimInputSpan.appendChild(delimEntryButton); // Insert the cloned section above the original Triggers section //triggersSection.parentNode.insertBefore(delimEntrySection, triggersSection); insertAfter(entrySection, delimEntrySection); delimEntryButton.onclick = () => { const startDelim = delimStartInput.value || delimStartInput.placeholder; const endDelim = delimEndInput.value || delimEndInput.placeholder; const selectionStart = entryField.selectionStart; const selectionEnd = entryField.selectionEnd; const currentValue = entryField.value; // Check if text is selected if (selectionStart !== selectionEnd) { // Bracket the selected text const newValue = currentValue.slice(0, selectionStart) + startDelim + currentValue.slice(selectionStart, selectionEnd) + endDelim + currentValue.slice(selectionEnd); entryField.focus(); textFieldInsert(entryField, newValue); entryField.focus(); entryField.setSelectionRange(selectionStart + startDelim.length, selectionEnd + startDelim.length); } else { // If no text is selected, insert delimiters at cursor position const newValue = currentValue.slice(0, selectionStart) + startDelim + endDelim + currentValue.slice(selectionStart); entryField.focus(); textFieldInsert(entryField, newValue); entryField.focus(); entryField.setSelectionRange(selectionStart + startDelim.length, selectionStart + startDelim.length); } }; triggersSection.parentNode.insertBefore(delimEntrySection, triggersSection); // Find the notes textarea element and set it to a default from monkey config. const notesTextArea = modalNode.querySelector("textarea[aria-label='Notes']"); if (notesTextArea) { const defaultSCNotes = cfg.get('Default_SC_Notes'); if (defaultSCNotes !== "") { if (notesTextArea && notesTextArea.value.trim() === "") { textFieldInsert(notesTextArea, defaultSCNotes); } } } GM_addStyle(` #scDelimInputSpan { /* Use the ID of the span */ display: flex !important; /* Use !important to override existing styles */ justify-content: space-between !important; align-items: center !important; /* Add this to vertically center items */ gap: 5px !important; } #scDelimInputSpan > #scDelimStartInput { width: 65% !important; /* Adjust as needed to control the width of the inputs */ } #scDelimInputSpan > #scDelimEndInput { /* Target the end delimiter specifically */ width: 25% !important; /* Set a narrower width for the end delimiter */ } #scDelimInputSpan > button { width: auto !important; /* Make the button expand to fit its content */ white-space: nowrap !important; } `); } function textFieldInsert(field, text) { setNativeValue(field, text); // Update the value using setNativeValue // Trigger an input event to notify React const inputEvent = new InputEvent('input', { bubbles: true }); field.dispatchEvent(inputEvent); } function unsetOverflowRecursively(node) { // Unset all overflow properties on the current node node.style.overflow = ''; node.style.overflowX = ''; node.style.overflowY = ''; // Recursively process child nodes for (const child of node.children) { unsetOverflowRecursively(child); } } const classListRemove = [ '_h-512px', '_mih-0px', '_miw-0px', '_fs-0', /* Padding we want removed */ '_pt-1481558400', '_pr-1481558400', '_pb-1481558400', '_pl-1481558400', 'r-150rngu', // -webkit-overflow-scrolling: touch; // (has error.) 'r-1rnoaur', // overflow-y: auto; // (we don't want auto scrolling on nested divs. have unset) 'r-11yh6sk', // overflow-x: auto; // (we don't want auto scrolling on nested divs. have unset) 'r-eqz5dr', // flex-direction: column; 'r-16y2uox', // flex-grow: 1; 'r-1wbh5a2', // flex-shrink: 1; 'r-agouwx' // transform: translateZ(0); ]; const classListRemove2 = [ '_mih-0px', '_miw-0px', '_fs-0', /* Padding we want removed */ '_pt-1481558400', '_pr-1481558400', '_pb-1481558400', '_pl-1481558400', 'r-150rngu', // -webkit-overflow-scrolling: touch; // (has error.) 'r-1rnoaur', // overflow-y: auto; // (we don't want auto scrolling on nested divs. have unset) 'r-11yh6sk', // overflow-x: auto; // (we don't want auto scrolling on nested divs. have unset) 'r-eqz5dr', // flex-direction: column; 'r-16y2uox', // flex-grow: 1; 'r-1wbh5a2', // flex-shrink: 1; 'r-agouwx' // transform: translateZ(0); ]; function classListRemoveRecursively(node, classList) { // Unset all overflow properties on the current node //classList.forEach(className => node.classList.remove(className)); // Recursively process child nodes for (const child of node.children) { classListRemoveRecursively(child); } } function fixStyles(modalNode) { const modalContent = modalNode.children[1]; classListRemove.forEach(className => modalContent.classList.remove(className)); } function makeModalDraggableAndResizable(timestamp, modalNodeTree, modalNode) { console.log("makeModalDraggableAndResizable"); const modalDimensions = cfg.get('Modal_Dimensions'); const [modalWidth, modalHeight] = modalDimensions; // Setup the initial modal dimensions from configure monkey. modalNode.style.width = `${modalWidth}px`; modalNode.style.maxWidth = `100%`; modalNode.style.height = `${modalHeight}px`; modalNode.style.maxHeight = `100%`; modalNode.style.minHeight = '0px'; modalNode.style.resize = 'both'; modalNode.style.overflowY = 'hidden'; modalNode.style.overflowX = 'hidden'; // Get some references to important things and assign id's const modalHeader = modalNode?.children[0]; const modalHeaderId = "modalHeader_" + timestamp; modalHeader.id = modalHeaderId; const modalContent = modalNode?.children[1]; modalContent.id = "modalContent_" + timestamp; let modalInnerContent = modalContent?.children[0]; let modalInnerContentId = null; modalInnerContentId = "modalInnerContent_" + timestamp; // Called by both fixModalContent, and by a mutation observer that watches the modal content // incase of changes. function fixModalInnerContent(timestamp, modalNode, modalInnerContent) { if (!modalInnerContent) return; console.log("fixModalInnerContent"); classListRemove.forEach(className => modalInnerContent.classList.remove(className)); const classListRemovez = [ '_mih-0px', '_miw-0px', '_fs-0', '_pt-1481558400', '_pr-1481558400', '_pb-1481558400', '_pl-1481558400', 'r-150rngu', 'r-1rnoaur', 'r-11yh6sk', '_mr-1481558369', 'r-agouwx' ]; //'_h-512px', // '_mih-0px', '_miw-0px', '_fs-0', // // Padding we want removed // '_pt-1481558400', '_pr-1481558400', '_pb-1481558400', '_pl-1481558400', // 'r-150rngu', // -webkit-overflow-scrolling: touch; // (has error.) // 'r-1rnoaur', // overflow-y: auto; // (we don't want auto scrolling on nested divs. have unset) // 'r-11yh6sk', // overflow-x: auto; // (we don't want auto scrolling on nested divs. have unset) // 'r-eqz5dr', // flex-direction: column; // 'r-16y2uox', // flex-grow: 1; // 'r-1wbh5a2', // flex-shrink: 1; // 'r-agouwx' // transform: translateZ(0); classListRemoveRecursively(modalInnerContent.firstChild, classListRemovez); modalInnerContent.id = modalInnerContentId; //modalInnerContent.id = "modalInnerContentId_" + timestamp; unsetOverflowRecursively(modalInnerContent); modalInnerContent.style.padding = '0px'; modalInnerContent.style.overflowX = 'unset'; modalInnerContent.style.overflowY = 'unset'; modalInnerContent.style.maxHeight = '100%'; modalInnerContent.style.maxWidth = '100%'; // When different tabs are selected, assign id's for CSS. if (modalHeader.querySelectorAll( 'div[role="tablist"][aria-label="Section Tabs"] div[role="tab"][aria-label^="Selected tab story cards" i]' ).length >= 1) { modalInnerContent.firstChild.id = 'modalInnerContent_storyCardsTab'; modalInnerContent.firstChild.firstChild.classList.remove('r-150rngu', 'r-1rnoaur', 'r-11yh6sk'); } else if (modalHeader.querySelectorAll( 'div[role="tablist"][aria-label="Section Tabs"] div[role="tab"][aria-label^="Selected tab plot" i]' ).length >= 1) { modalInnerContent.firstChild.id = 'modalInnerContent_plotTab'; } else if (modalHeader.querySelectorAll( 'div[role="tablist"][aria-label="Section Tabs"] div[role="tab"][aria-label^="Selected tab details" i]' ).length >= 1) { modalInnerContent.firstChild.id = 'modalInnerContent_detailsTab'; } } if (modalInnerContent) { modalInnerContent.id = modalInnerContentId; modalInnerContent.style.overflowX = 'unset'; modalInnerContent.style.overflowY = 'unset'; modalInnerContent.style.maxHeight = '100%'; modalInnerContent.style.maxWidth = '100%'; } else { console.log("modalInnerContent Failed."); } setTimeout(() => { fixStyles(modalNode); fixModalInnerContent(timestamp, modalNode, modalInnerContent); // Center the modal initially (after a slight delay for rendering) // To allow dragging and resizing the modal, we have to disable centering. // But we don't want the modal to jump to the top left of the viewport. // So we center it manually. if (modalNodeTree) { const modalRect = modalNode.getBoundingClientRect(); const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; const left = (viewportWidth - modalRect.width) / 2.0; const top = (viewportHeight - modalRect.height) / 2.0; // The path to the div that is centering the modal preventing it from being moved. const centeringParent = modalNodeTree.querySelector('div > span > span > div'); // Remove potentially conflicting classes before centering centeringParent.classList.remove('_ai-center', '_jc-center', '_pos-fixed'); // Apply initial left and top positions modalNode.style.left = `${Math.max(0, left)}px`; // Ensure left is not negative modalNode.style.top = `${Math.max(0, top)}px`; // Ensure top is not negative // Override centering styles on the parent (AFTER centering the modal) centeringParent.style.justifyContent = 'unset'; centeringParent.style.alignItems = 'unset'; } if (modalInnerContent) { modalInnerContentId = "modalInnerContent_" + timestamp; modalInnerContent.id = modalInnerContentId; if (1) { // Turned off to experiment with using the original scroller. const originalScroller = modalNode.closest('[data-remove-scroll-container="true"]'); if (originalScroller) { originalScroller.style.overflowY = 'hidden'; // Disable scrolling on the original element originalScroller.removeAttribute('data-remove-scroll-container'); // Remove the attribute } else { console.warn("Original scrolling element not found in modal."); } } } else { console.warn("Content div not found in modal after delay."); } }, 100); // Use a MutationObserver to monitor changes in the modal's content const observer = new MutationObserver((mutationsList, observer) => { for (let mutation of mutationsList) { if (mutation.type === 'childList') { //let modalNode.offsetWidth; // Trigger a reflow let newModalInnerContent = modalNode?.children[1]?.children[0]; // Look for updated content div if (newModalInnerContent) { modalInnerContent = newModalInnerContent; fixModalInnerContent(timestamp, modalNode, newModalInnerContent); // Apply styles to the new inner content element } } } }); observer.observe(modalNode, { childList: true, subtree: true }); // Observe all child nodes let startX = null; let startY = null; // New event listeners for touch events (passive) for dragging modalHeader.addEventListener('mousedown', handleDragStart); modalHeader.addEventListener('touchstart', handleDragStart, { passive: true }); // Prevent default behavior for touch events to avoid scrolling and interfering with dragging modalNode.addEventListener('touchmove', (e) => e.preventDefault(), { passive: false }); function handleDragStart(e) { isDragging = true; startX = e.clientX - modalNode.offsetLeft; startY = e.clientY - modalNode.offsetTop; document.addEventListener('mousemove', handleDragMove); document.addEventListener('mouseup', handleDragEnd); console.log("handleDragStart"); // Prevent default behavior for touch events to avoid scrolling if (e.type === 'touchstart') { e.preventDefault(); } e.stopPropagation(); // Stop event propagation to prevent conflicts } function handleDragMove(e) { if (!isDragging) return; const x = e.clientX || e.touches[0].clientX; // Get x position for mouse or touch const y = e.clientY || e.touches[0].clientY; // Get y position for mouse or touch modalNode.style.left = `${x - startX}px`; modalNode.style.top = `${y - startY}px`; } function handleDragEnd(e) { isDragging = false; document.removeEventListener('mousemove', handleDragMove); document.removeEventListener('mouseup', handleDragEnd); console.log("handleDragEnd"); } /* // This is an alternate approach that handles touch events. // But it is a bit jumpy and needs work. modalHeader.addEventListener('mousedown', handleDragStart); modalHeader.addEventListener('touchstart', handleDragStart); // Remove passive here let activeTouches = 0; // Counter for active touch points function handleDragStart(e) { activeTouches++; if (activeTouches > 1) return; // Allow multi-touch for zoom/pinch gestures if (e.type === 'touchstart') { startX = e.touches[0].clientX - modalHeader.getBoundingClientRect().left; startY = e.touches[0].clientY - modalHeader.getBoundingClientRect().top; } else { startX = e.offsetX; startY = e.offsetY; } // Add touchmove and touchend listeners ONLY when touchstart occurs if (e.type === 'touchstart') { document.addEventListener('touchmove', handleDragMove); document.addEventListener('touchend', handleDragEnd); } else { // For mousedown document.addEventListener('mousemove', handleDragMove); document.addEventListener('mouseup', handleDragEnd); } console.log("handleDragStart e.type: ", e.type); } function handleDragMove(e) { if (activeTouches > 1) return; // Allow multi-touch for zoom/pinch gestures const x = e.clientX || e.touches[0].clientX; // Get x position for mouse or touch const y = e.clientY || e.touches[0].clientY; // Get y position for mouse or touch modalNode.style.left = `${x - startX}px`; modalNode.style.top = `${y - startY}px`; e.stopPropagation(); // Stop event propagation ONLY during dragging } function handleDragEnd(e) { activeTouches--; if (activeTouches > 0) return; // Wait for all touch points to be released // Remove touchmove and touchend listeners when touch ends if (e.type === 'touchend') { document.removeEventListener('touchmove', handleDragMove); document.removeEventListener('touchend', handleDragEnd); } else { // For mouseup document.removeEventListener('mousemove', handleDragMove); document.removeEventListener('mouseup', handleDragEnd); } console.log("handleDragEnd e.type: ", e.type); } */ } /* // Global variables let isGearMenuResizing = false; let gearMenuStartX, gearMenuStartWidth; // Get reference to app-root div const appRootDiv = document.querySelector('.app-root'); const gearMenuSelector = 'body > div.app-root > div#__next > div > span > div:nth-child(1) > div:nth-child(1)'; // MutationObserver for detecting the gear menu within the app-root div const appRootObserver = new MutationObserver((mutationsList, observer) => { console.log("appRootObserver Got Here:"); for (let mutation of mutationsList) { if (mutation.type === 'childList') { console.log("appRootObserver Got Here:2"); const gearMenu = document.querySelector(gearMenuSelector); // Target the gear menu if (gearMenu) { console.log("appRootObserver Got Here:3"); //makeGearMenuDraggableAndResizable(gearMenu); observer.disconnect(); // Stop observing after the gear menu is found } } } }); // MutationObserver for detecting changes in the Adventure tabs const adventureTabObserver = new MutationObserver((mutationsList, observer) => { console.log("adventureTabObserver Got Here:"); for (let mutation of mutationsList) { if (mutation.type === 'childList') { const plotTab = document.querySelector('div[role="tablist"][aria-label="Section Tabs" i] [role="tab"][aria-label*="Plot" i]'); const detailsTab = document.querySelector('div[role="tablist"][aria-label="Section Tabs" i] [role="tab"][aria-label*="Details" i]'); if (plotTab && plotTab.getAttribute('aria-selected') === 'true') { console.log("adventureTabObserver Got Here: 1"); //resizeAllTextareasInNode(plotTab); } else if (detailsTab && detailsTab.getAttribute('aria-selected') === 'true') { console.log("adventureTabObserver Got Here: 2"); //resizeAllTextareasInNode(detailsTab); } } } }); function makeGearMenuDraggableAndResizable(gearMenu) { const resizeHandle = document.createElement('div'); resizeHandle.classList.add('resize-handle'); resizeHandle.style.left = 0; // Position on the left edge gearMenu.appendChild(resizeHandle); function toggleFullScreen(buttonTextElement) { const modalRect = modalNode.getBoundingClientRect(); if (modalNode.requestFullscreen) { if (document.fullscreenElement) { document.exitFullscreen(); buttonTextElement.innerText = "[ ]"; // Restore previous size and position (ensure consistent pixel values) modalNode.style.width = modalNode.dataset.originalWidth + 'px'; modalNode.style.height = modalNode.dataset.originalHeight + 'px'; modalNode.style.left = modalNode.dataset.originalLeft + 'px'; modalNode.style.top = modalNode.dataset.originalTop + 'px'; // Force reflow to ensure styles are applied correctly void modalNode.offsetWidth; // Trigger a reflow } else { // Store current size and position before going fullscreen (ensure pixel values) modalNode.dataset.originalWidth = modalRect.width; modalNode.dataset.originalHeight = modalRect.height; modalNode.dataset.originalLeft = modalRect.left; modalNode.dataset.originalTop = modalRect.top; modalNode.requestFullscreen(); buttonTextElement.innerText = "[X]"; } } } function createFullScreenButton(cloneRef, container) { if (!cloneRef) return; const fullScreenButton = buttonClone( cloneRef, "[ ]", // Button label (you can customize this) toggleFullScreen // Function to handle the toggle ); container.insertBefore(fullScreenButton, container.firstChild); container.style.display = 'flex'; container.style.flexDirection = 'row'; container.style.alignItems = 'center'; } // Add fullscreen button (reusing existing createFullScreenButton function) const header = gearMenu.querySelector('[role="tablist"]'); // Find the header within the gear menu //const header = gearMenu.querySelector('[role="button"][aria-label="Close settings" i]'); // Find the header within the gear menu const closeButton = gearMenu.querySelector('[role="button"][aria-label="Close settings" i]').parentElement; // Find the header within the gear menu if (header) { const fullScreenButton = createFullScreenButton(closeButton, closeButton.parentElement.ParentElement); header.insertBefore(fullScreenButton, header.firstChild); } resizeHandle.addEventListener('mousedown', handleResizeStart); resizeHandle.addEventListener('touchstart', handleResizeStart, { passive: true }); function handleResizeStart(e) { isGearMenuResizing = true; gearMenuStartX = e.clientX; gearMenuStartWidth = parseInt(document.defaultView.getComputedStyle(gearMenu).width, 10); document.addEventListener('mousemove', handleResizeMove); document.addEventListener('mouseup', handleResizeEnd); // Prevent default behavior for touch events to avoid scrolling if (e.type === 'touchstart') { e.preventDefault(); } } // Modified resize logic (horizontal resizing only) function handleResizeMove(e) { if (!isGearMenuResizing) return; const x = e.clientX || e.touches[0].clientX; const newWidth = gearMenuStartWidth + (x - gearMenuStartX); gearMenu.style.width = `${Math.max(200, newWidth)}px`; // Minimum width of 200px } function handleResizeEnd(e) { isGearMenuResizing = false; document.removeEventListener('mousemove', handleResizeMove); document.removeEventListener('mouseup', handleResizeEnd); } } */ /* // Start observing the app-root div if (appRootDiv) { appRootObserver.observe(appRootDiv, { childList: true, subtree: true }); } else { console.warn("app-root div not found."); } */ // ... (rest of your existing code) ... function modalAddFullScreenButton(cloneRef, container, eventHandler) { if (!cloneRef) { console.warn("Null cloneRef in modalAddFullScreenButton!"); return; } const fullScreenButton = buttonClone( cloneRef, "[ ]", // Button label (you can customize this) eventHandler // Function to handle the toggle ); container.insertBefore(fullScreenButton, container.lastChild); container.style.display = 'flex'; container.style.flexDirection = 'row'; container.style.alignItems = 'center'; container.style.justifyContent = 'unset'; // Remove default justification //fullScreenButton.style.marginRight = '8px'; fullScreenButton.style.minWidth = '30px'; fullScreenButton.style.whiteSpace = 'nowrap'; container.style.display = 'flex'; container.style.alignItems = 'right'; container.style.flexGrow = '1'; container.style.justifyContent = 'flex-end'; } function toggleFullScreen(buttonTextElement) { const modalNode = buttonTextElement.closest("div[aria-label='Modal' i]"); if (!modalNode) { console.error("Error: Modal node not found. Fullscreen toggle failed."); return; } const modalRect = modalNode.getBoundingClientRect(); if (modalNode.requestFullscreen) { if (document.fullscreenElement) { document.exitFullscreen(); buttonTextElement.innerText = "[ ]"; // Restore previous size and position (ensure consistent pixel values) modalNode.style.width = modalNode.dataset.originalWidth + 'px'; modalNode.style.height = modalNode.dataset.originalHeight + 'px'; modalNode.style.left = modalNode.dataset.originalLeft + 'px'; modalNode.style.top = modalNode.dataset.originalTop + 'px'; // Force reflow to ensure styles are applied correctly void modalNode.offsetWidth; // Trigger a reflow } else { // Store current size and position before going fullscreen (ensure pixel values) modalNode.dataset.originalWidth = modalRect.width; modalNode.dataset.originalHeight = modalRect.height; modalNode.dataset.originalLeft = modalRect.left; modalNode.dataset.originalTop = modalRect.top; modalNode.requestFullscreen(); buttonTextElement.innerText = "[X]"; } } } // Function to handle new modals // function handleNewModal(modalNodeTree) { const timestamp = Date.now(); // Wait for the specific modal structure waitForSubtreeElements( "div[aria-label='Modal' i]", (modalNodes) => { if (modalNodes.length !== 1) { console.warn("Modal nodes, there can be only 1. Found: ", modalNodes.length); return; } const modalNode = modalNodes[0]; if (!modalNode) { console.warn("Null Modal node found in handleNewModle."); return; } modalNodeTree.id = "modalNodeTree"; modalNode.style.padding = 0; modalNode.style.margin = 0; modalNode.style.borderBottomRightRadius = 0; waitForSubtreeElements( "div[aria-label='Modal' i] > div:nth-child(2) button", // The selector for the element you want to wait for within the modal //"div[aria-label='Modal' i] > div > div", // The selector for the element you want to wait for within the modal //"div[aria-label='Modal' i]:has(> div:nth-child(2))", // Wait for the 2nd child to appear. (modalSubNodes) => { setTimeout(() => { // Need to wait some time for react to render the contents and post it. const modalHeader = modalNode?.children[0]; /* Assign IDs for CSS. */ const modalContent = modalNode?.children[1]; modalContent.id = "modalContent_" + timestamp; const modalHeaderTitleContainer = modalHeader?.firstChild; modalHeaderTitleContainer.id = "modalHeaderTitleContainer_" + timestamp; const modalInnerContent = modalContent?.children[0]; modalInnerContent.id = "modalInnerContent" + timestamp; // Add resizing and dragging for edit Adventure and Scenario modals. const tablistSelector = 'div[role="tablist"][aria-label="Section Tabs"] [role="tab"][aria-label*="plot" i], ' + 'div[role="tablist"][aria-label="Section Tabs"] [role="tab"][aria-label*="Story Cards" i],' + 'div[role="tablist"][aria-label="Section Tabs"] [role="tab"][aria-label*="details" i],' + 'div[role="button"][aria-label="Close modal" i] > div > p'; if (modalNode.querySelectorAll(tablistSelector).length >= 4) { //centeringParent.classList.remove('_ai-center', '_jc-center', '_pos-fixed'); if (modalHeaderTitleContainer) { //modalHeaderTitleContainer.style.justifyContent = 'space-between'; } waitForSubtreeElements( tablistSelector, (matchingElements) => { if (matchingElements.length >= 4) { // Check if all 3 tabs are found modalNodeTree.id += ".ScenarioAdventureEditor"; makeModalDraggableAndResizable(timestamp, modalNodeTree, modalNode); // Find the nested button setTimeout(() => { let closeButton = null; let container = null; //modalInnerContent.style.justifyContent = 'space-between'; //modalHeaderTitleContainer.style.justifyContent = 'space-between'; // Check for double button first. let closeButtons = modalNode.querySelectorAll( "button[role='button'][type='button' i] div[role='button'][aria-label='Close modal' i]"); if (closeButtons.length > 0) { closeButton = closeButtons[0]; // Keep the pointer to the inner div button for cloning. // Find the enclosing button element. container = closeButton.closest("button[role='button'][type='button' i]")?.parentNode; } // It's not a double button. else { closeButtons = modalNode.querySelectorAll("div[role='button'][aria-label='Close modal' i]"); if (closeButtons.length > 0) { closeButton = closeButtons[0]; container = closeButton.parentNode; } } if (!closeButton || !container) { console.error("Error: Close button not found in modal. Fullscreen button not added."); console.log(closeButton); console.log(container); } else { modalAddFullScreenButton(closeButton, container, toggleFullScreen); container.style.justifyContent = 'space-between'; modalInnerContent.style.minHeight = '0px'; } }, 100); // adjust as needed } }, modalNode, true ); } // Story card updates. else if (modalNode.querySelectorAll("textarea[aria-labelledby='scEntryLabel']").length >= 1) { //console.log("Found story card modal"); //console.log("scEntryLable", modalNode); modalNodeTree.id += ".StoryCardEditor"; modalHeader.padding = '8px'; setTimeout(() => { modalContent.style.maxHeight = 'calc(100% - ' + modalHeader.offsetHeight + 'px)'; makeModalDraggableAndResizable(timestamp, modalNodeTree, modalNode); modifyStoryCardEditor(modalNode); const closeButton = modalNode.querySelector("div[role='button'][aria-label='Close modal' i]"); modalAddFullScreenButton(closeButton, modalHeader, toggleFullScreen); }, 100); // Adjust delay as needed // Add additional story card updates here... // } // #content-\:r6ji\: > div > div._dsp-flex._fb-auto._bxs-border-box._pos-relative._mih-0px._miw-0px._fs-0._pr-1481558369._pl-1481558307._pt-1481558338._pb-1481558338._gap-1481558338._w-10037._fd-row._ai-center._jc-441309761._bbw-0px._btc-43811612._brc-43811612._bbc-43811612._blc-43811612._maw-480px._btw-0px._brw-0px._blw-0px._bbs-solid._bts-solid._bls-solid._brs-solid > div > div.is_Row._dsp-flex._fd-row._fb-auto._bxs-border-box._pos-relative._mih-0px._miw-0px._fs-0._ai-center._jc-441309761 > div > h1 else if ($(modalHeader).find("h1:contains('Adventure')").length > 0) { setTimeout(() => { modalNodeTree.id += ".ContentView.Adventure"; makeModalDraggableAndResizable(timestamp, modalNodeTree, modalNode); }, 100); // adjust as needed } // Fix the irritating small window size for the script editor. // else if ($(modalNode).find("p:contains('Shared Library')").length > 0) { setTimeout(() => { waitForKeyElements(".monaco-editor .view-lines", (editorElements) => { modalNodeTree.id += ".ScriptEditor"; const editorContainer = modalNode.children[1]; if (editorContainer) { editorContainer.style.height = "90%"; } else { console.warn("Editor container div not found in script editor modal"); } //modalNode.dataset.hasEditorMods = "true"; }, true); }, 100); // adjust as needed } else { //console.log("Found other modal", modalNode); // ... you can add handlers for other types of modals here ... } }, 500); // adjust as needed //}, 1); // adjust as needed }, modalNode, // Use the modal node as the targetNode true // Run immediately. ); }, modalNodeTree, // Use the modal node as the targetNode true // Run immediately. ); } // Mutation observer to detect new div elements in document.body const bodyObserver = new MutationObserver((mutationsList, observer) => { for (const mutation of mutationsList) { if (mutation.type === 'childList') { for (const node of mutation.addedNodes) { if (node.nodeName === 'DIV') { // Check for modals and new buttons if (node.querySelector("div[aria-label='Modal' i]")) { console.log("New modal detected in document.body"); handleNewModal(node); } } } } } }); // Start observing the document body for new children added. bodyObserver.observe(document.body, { childList: true }); /********************************** ** Code for Play Pages. */ //let modalMutationObserver = new DOMObserver(modalMutationObserverCB, document.body, { childList: true, subtree: true }); //modalMutationObserver.observe(); // Fist the state.message display locking out the Navigation bar. // function fixNavigationBar() { const navBar = document.querySelector('div[aria-label="Navigation bar"]'); const dialog = document.querySelector('.css-175oi2r[style*="z-index: 3"]'); //const circleInfoDiv = dialog?.querySelector('p.font_icons[aria-hidden="true"]:has(+ p:contains("w_circle_info"))')?.closest('div[role="button"]'); // Find the closest parent div with role="button" if (navBar && dialog) { navBar.style.zIndex = 100011; // Higher than the overlay pane dialog.style.zIndex = 2; // Lower z-index for the dialog dialog.id = 'TheDialog'; const alertContainer = navBar.querySelectorAll('div[role="alert"]'); const nestedOverlay = dialog.querySelector('div[style*="z-index: 100000"]'); // Select nested overlay if (nestedOverlay) { nestedOverlay.style.zIndex = 1; // Set a lower z-index for the nested overlay //nestedOverlay.id = 'TheNestedOverlay' } const navBarButtons = navBar.querySelectorAll('div[id="game-blur-button"]'); navBarButtons.forEach(button => { button.style.zIndex = 100012; // Even higher for the buttons const buttonZi1Elements = button.querySelectorAll('._zi-1'); buttonZi1Elements.forEach(element => { element.style.zIndex = 100013; // Highest z-index for elements inside buttons }); }); const navBarZi1Elements = navBar.querySelectorAll('._zi-1'); navBarZi1Elements.forEach(element => { element.style.zIndex = 100011; // Match the navBar's z-index }); // Find the 'alert' div. const alertDialog = dialog.querySelector("div[role='alert']"); // Add click listener to dismiss the dialog when the circle info div is clicked const $circleInfoDiv = $(dialog).find( "div[role='alert'] > div > div > div:first-child > div:first-child" ); if ($circleInfoDiv.length > 0) { $circleInfoDiv.on("click", () => { dialog.remove(); }); } // Add click listener to dismiss the dialog when the circle info div is clicked const circleInfoDiv = dialog.querySelector("div[role='alert'] > div > div:first-child"); if (circleInfoDiv) { console.log("found circleInfoDiv:", circleInfoDiv); circleInfoDiv.addEventListener("click", () => { dialog.remove(); }); } } } let fixNavigationBarObserver = new DOMObserver( fixNavigationBar, document.body, { childList: true, subtree: true } ); function handlePlayPage(targetNode) { // handleChanges(); handleChangesObserver = new DOMObserver(handleChanges, targetNode, { childList: true, subtree: true }); handleChangesObserver.observe(); const CSS = ` div>span:last-child>#transition-opacity:last-child, #game-backdrop-saturate { border-bottom-color: ${cfg.get('Response_Underline') ? 'var(--color-61)' : 'unset'}; border-bottom-width: ${cfg.get('Response_Underline') ? '2px' : 'unset'}; border-bottom-style: ${cfg.get('Response_Underline') ? 'solid' : 'unset'}; background-color: ${cfg.get('Response_Bg_Color') ? 'var(--color-60)' : 'unset'}; backdrop-filter: unset; } `; GM_addStyle(CSS); const referenceSpan = [...$('[role=button]')].find((e) => e.innerText === 'w_undo').parentElement; // Select the span const container = referenceSpan.parentElement; headerInject(container, referenceSpan, toggleButtonText, toggleOnClick); fixNavigationBarObserver.observe(); } document.addEventListener('keydown', handleKeyPress); waitForKeyElements("[role='article']", (targetNodes) => { const targetNode = targetNodes[0]; const isReadPage = window.location.href.includes('/read'); const isPlayPage = window.location.href.includes('/play'); if (isReadPage || isPlayPage) { // This is setup code for both read and play pages. //targetNode = $("[role='article']")[0]; // Load toggle state from sessionStorage (default to ON if not found) actionsExpanded = sessionStorage.getItem("actionsExpanded") === "true"; toggleButtonText = actionsExpanded ? ActionToggleMsgOn : ActionToggleMsgOff; setActionVisibility(actionsExpanded); // Set the visibility from the session saved state. if (isReadPage) { handleReadPage(targetNode); } else if (isPlayPage) { handlePlayPage(targetNode); } } });
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址