AIDungeon QoL Tool10

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.greasyfork.org/scripts/383527/701631/Wait_for_key_elements.js
// @require      https://update.greasyfork.org/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://greasyfork.org/users/1302066
// ==/UserScript==

/* global jQuery, $, waitForKeyElements, MonkeyConfig */

/// require      https://cdn.jsdelivr.net/npm/tampermonkey-require-for-react
/// @downloadURL https://update.greasyfork.org/scripts/1302066/AIDungeon%20QoL%20Tool.user.js
/// @updateURL https://update.greasyfork.org/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);
    }
  }

});