MangaDex Condensed

Enhance MangaDex with lots of display options to make it easier to find unread chapters.

// ==UserScript==
// @name         MangaDex Condensed
// @namespace    suckerfree
// @license      MIT
// @version      49
// @description  Enhance MangaDex with lots of display options to make it easier to find unread chapters.
// @author       Nalin
// @match        https://mangadex.org/*
// @icon         https://www.google.com/s2/favicons?domain=mangadex.org
//
// @require      https://cdn.jsdelivr.net/gh/sizzlemctwizzle/GM_config@2207c5c1322ebb56e401f03c2e581719f909762a/gm_config.js
// @grant        GM_getValue
// @grant        GM_setValue
// ==/UserScript==

(function() {
  // Configure GM_config.
  GM_config.init(
  {
    'id': 'MangaDexCondensed',
    'fields': {
      'CoverMode': {
        'label': 'Popup/Enlarge Cover When Hovered On',
        'type': 'select',
        'options': ['Container', 'Title + Cover', 'Title', 'Cover', 'Never'],
        'default': 'Cover'
      },
      'CoverStyle': {
        'label': 'Preview Cover Style',
        'type': 'select',
        'options': ['Small', 'Full Size', 'Hidden'],
        'default': 'Small'
      },
      'CoverExpandDirection': {
        'label': 'Cover Expands',
        'type': 'select',
        'options': ['Down', 'Float Up', 'Float Down'],
        'default': 'Float Up'
      },
      'ReadChapterStyle': {
        'label': 'Read Chapter Style',
        'type': 'select',
        'options': ['Darken Background', 'Lighten Text', 'Hide'],
        'default': 'Darken Background'
      },
      'LeftClickMode': {
        'label': 'Left Click Opens In',
        'type': 'select',
        'options': ['Same Window', 'New Window'],
        'default': 'Same Window'
      },
      'CondenseElements': {
        'label': 'Condense Page Elements (reduce whitespace)',
        'type': 'checkbox',
        'default': true
      },
      'CondenseFonts': {
        'label': 'Adjust Font Sizes and Weights',
        'type': 'checkbox',
        'default': true
      }
    },
    'events': {
      'open': function() {
        const s = GM_config.frame.style;
        s.inset = '100px auto auto calc(50% - 500px/2)';
        s.width = '500px';
        s.height = '310px';
      },
      'save': function() { location.reload(); },
      'reset': function() { location.reload(); }
    },
    'css': '#MangaDexCondensed_header { margin-bottom: 15px !important; }'
  });

  // Adds a style to the <head> tags.
  function addGlobalStyle(css) {
    const head = document.getElementsByTagName('head')[0];
    if (!head) { return; }
    const style = document.createElement('style');
    style.type = 'text/css';
    style.setAttribute('from', 'mdc');
    style.innerHTML = css;
    head.appendChild(style);
  }

  // Creates the settings button.
  function createSettingsButton(divData, svgData) {
    const config = document.createElement('button');
    const icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    const path1 = document.createElementNS('http://www.w3.org/2000/svg', 'path');
    const path2 = document.createElementNS('http://www.w3.org/2000/svg', 'path');

    // Settings gear icon uses the CC0 (public domain) license.
    // https://www.svgrepo.com/svg/201666/settings-gear

    path1.setAttribute('stroke', 'currentColor');
    path1.setAttribute('stroke-width', '1');
    path1.setAttribute('d', `M491.584,192.579l-55.918-6.914c-0.919-2.351-1.884-4.682-2.892-6.993l34.648-44.428
      c7.227-9.267,6.412-22.464-1.899-30.773l-57.028-56.996c-8.308-8.304-21.502-9.114-30.763-1.893L333.32,79.216
      c-2.312-1.008-4.644-1.974-6.994-2.894l-6.915-55.904c-1.443-11.66-11.348-20.415-23.097-20.415h-80.637
      c-11.748,0-21.656,8.755-23.097,20.416l-6.914,55.904c-2.349,0.919-4.681,1.884-6.988,2.89l-44.415-34.642
      c-9.261-7.222-22.458-6.414-30.768,1.894l-57.021,57.009c-8.31,8.307-9.123,21.506-1.896,30.771l34.644,44.417
      c-1.012,2.312-1.978,4.647-2.9,7.002l-55.906,6.914C8.757,194.022,0,203.927,0,215.676v80.64c0,11.75,8.758,21.658,20.421,23.097
      l55.901,6.903c0.919,2.352,1.884,4.686,2.894,6.994l-34.641,44.417c-7.224,9.264-6.411,22.46,1.894,30.767l57.021,57.031
      c8.307,8.31,21.507,9.121,30.773,1.896l44.417-34.648c2.306,1.007,4.638,1.974,6.987,2.891l6.914,55.921
      c1.441,11.66,11.348,20.416,23.097,20.416h80.637c11.748,0,21.655-8.755,23.097-20.416l6.915-55.92
      c2.351-0.92,4.682-1.885,6.993-2.892l44.425,34.65c9.266,7.225,22.463,6.414,30.771-1.898l57.015-57.031
      c8.307-8.308,9.117-21.504,1.893-30.768l-34.641-44.409c1.012-2.313,1.978-4.647,2.898-7.002l55.901-6.903
      c11.661-1.44,20.421-11.348,20.421-23.097v-80.64C512,203.927,503.243,194.022,491.584,192.579z M465.455,275.74l-49.864,6.158
      c-9.151,1.131-16.772,7.556-19.431,16.386c-2.813,9.337-6.56,18.387-11.138,26.903c-4.367,8.124-3.525,18.063,2.147,25.335
      l30.898,39.613l-27.924,27.932l-39.621-30.905c-7.269-5.668-17.202-6.513-25.327-2.15c-8.513,4.572-17.565,8.319-26.905,11.134
      c-8.827,2.661-15.25,10.279-16.381,19.427l-6.169,49.883h-39.492l-6.167-49.883c-1.131-9.146-7.551-16.763-16.375-19.425
      c-9.367-2.825-18.417-6.571-26.899-11.132c-8.122-4.369-18.061-3.527-25.336,2.147l-39.615,30.902L93.929,390.13l30.897-39.618
      c5.671-7.273,6.513-17.206,2.147-25.328c-4.568-8.501-8.315-17.554-11.137-26.911c-2.662-8.825-10.282-15.247-19.43-16.376
      l-49.861-6.156v-39.492l49.866-6.167c9.146-1.131,16.763-7.551,19.423-16.375c2.824-9.356,6.572-18.406,11.143-26.9
      c4.374-8.124,3.533-18.067-2.143-25.342l-30.903-39.62l27.924-27.918l39.62,30.902c7.273,5.672,17.209,6.513,25.335,2.146
      c8.493-4.565,17.541-8.31,26.896-11.132c8.825-2.662,15.247-10.279,16.378-19.427l6.166-49.867h39.494l6.169,49.869
      c1.133,9.148,7.557,16.767,16.384,19.427c9.328,2.811,18.379,6.557,26.902,11.135c8.122,4.364,18.055,3.522,25.325-2.149
      l39.616-30.894l27.927,27.912l-30.897,39.618c-5.666,7.267-6.513,17.191-2.158,25.311c4.58,8.54,8.328,17.599,11.138,26.923
      c2.661,8.825,10.279,15.248,19.427,16.381l49.878,6.169V275.74z`);

    path2.setAttribute('stroke', 'currentColor');
    path2.setAttribute('stroke-width', '1');
    path2.setAttribute('d', `M255.997,155.153c-55.606,0-100.845,45.244-100.845,100.856c0,55.603,45.239,100.839,100.845,100.839
      c55.609,0,100.852-45.236,100.852-100.839C356.849,200.397,311.606,155.153,255.997,155.153z M255.997,310.303
      c-29.941,0-54.3-24.356-54.3-54.294c0-29.947,24.359-54.311,54.3-54.311c29.944,0,54.306,24.363,54.306,54.311
      C310.303,285.947,285.941,310.303,255.997,310.303z`);

    icon.classList.add('icon', 'text-icon-contrast', 'text-undefined');
    icon.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
    icon.setAttribute('width', '24');
    icon.setAttribute('height', '24');
    icon.setAttribute('viewBox', '0 0 512 512');
    icon.setAttribute('fill', 'currentColor');

    config.classList.add('condensed-settings');
    config.addEventListener('click', function() { GM_config.open(); });

    if (svgData !== undefined) {
      path1.setAttribute(svgData, '');
      path2.setAttribute(svgData, '');
      icon.setAttribute(svgData, '');
    }
    if (divData !== undefined) {
      icon.setAttribute(divData, '');
      config.setAttribute(divData, '');
    }

    icon.append(path1);
    icon.append(path2);
    config.append(icon);

    return config;
  }

  // Helper function for finding data- attributes.
  const findDataAttribute = function(e) {
    for (const attribute of e.attributes) {
      if (attribute.name.startsWith('data-'))
        return attribute.name;
      return null;
    }
  }

  // Function for toggling a read chapter on mouse click.
  const toggleRead = function(ev) {
    //debugger;
    const tag = ev.target.tagName.toUpperCase();
    if (['SVG', 'PATH'].includes(tag)) return;
    if (ev.target.classList.contains('group-tag')) return;
    if (ev.target.classList.contains('user-tag')) return;
    if (ev.target.classList.contains('pill')) return;
    if (ev.target.closest('a.comment-container') !== null) return;
    if (ev.target.closest('.read') !== null) return;
    //if (ev.target.attributes.title?.value.includes('comments') ?? false) return;
    const chapter = ev.target.closest('.chapter');
    if (chapter === null) return;
    const ind = chapter.getElementsByTagName('svg')[0];
    if (ind !== undefined)
      ind.dispatchEvent(new MouseEvent('click'));
  }

  const removeElementEvents = function(elements) {
    // Assemble the list of elements to check.
    // JS really sucks sometimes.
    let arr = [];
    if (elements instanceof NodeList)
      arr.push(Array.from(elements));
    else arr.push([elements] || []);

    let clones = [];

    arr.flat(Infinity).forEach((element) => {

      // Clone the node in question.
      const clone = element.cloneNode(false);

      // Move all the children over to the clone and replace the original.
      [...element.childNodes].forEach((n) => clone.appendChild(n));
      element.replaceWith(clone);
      clones.push(clone);
    });

    return clones;
  }

  const rebindLeftClick = function(chapter) {
    [...chapter.querySelectorAll('a')].forEach((a) => a.setAttribute('target', '_blank'));
  }

  // Store this so when we change pages, we can disconnect it.
  let current_page_observers = [];
  let previous_pathname = '';

  ///////////////////////////////////////////////////////////////////////////////

  function addStyles() {
    // Follow.
    {
      const style = `
        /* Thin out the container padding. */
        #__nuxt[mdcpage="follow"][mdcce="true"] .chapter-feed__container {padding: 0.25rem !important;}

        /* Adjust the location of the cover image. */
        #__nuxt[mdcpage="follow"][mdccover="Small"] .chapter-feed__container {grid-template-columns: 41px minmax(0,1fr) !important;}
        #__nuxt[mdcpage="follow"][mdccover="Small"] .chapter-feed__cover {width: 41px !important; height: 53px !important; max-height: initial !important; padding-bottom: 0px !important;}
        #__nuxt[mdcpage="follow"][mdccover="Hidden"] .chapter-feed__container {grid-template-areas: "title title" "list list" !important;}
        #__nuxt[mdcpage="follow"][mdccover="Hidden"] .chapter-feed__cover {display:none;}

        /* Remove bolding of the chapter titles. */
        /* Adjust the font size of the title. */
        #__nuxt[mdcpage="follow"][mdccf="true"] .chapter-link {font-weight: normal !important; font-size: 0.75rem !important;}

        /* Cover expansion. */
        #__nuxt[mdcpage="follow"][mdccoverenabled="true"] .chapter-feed__container.mdc-cover-expand {grid-template-columns: 140px minmax(0,1fr) !important; position: relative;}
        #__nuxt[mdcpage="follow"][mdccoverenabled="true"] .chapter-feed__container.mdc-cover-expand a.chapter-feed__cover {width: 140px !important; height: 196px !important;}
        #__nuxt[mdcpage="follow"][mdccoverenabled="true"] .mdc-cover-expand .chapter-feed__cover {display:revert;}
        #__nuxt[mdcpage="follow"][mdccoverenabled="true"][mdccover="Hidden"] .chapter-feed__container.mdc-cover-expand {grid-template-areas: "art title" "art list" !important;}
        #__nuxt[mdcpage="follow"][mdccoverenabled="true"][mdccoverfloat="true"] .mdc-cover-expand:not(.expand) .chapter-feed__cover {outline: 4px solid rgb(var(--md-accent)); position: absolute; z-index: 1;}
        #__nuxt[mdcpage="follow"][mdccoverenabled="true"][mdccoverfloat="true"][mdcstyle="Darken Background"] .mdc-cover-expand.condensed-read:not(.expand) .chapter-feed__cover {outline-color: rgb(var(--mdc-read-background)) !important;}
        #__nuxt[mdcpage="follow"][mdccoverenabled="true"][mdccoverexpand="Float Up"] .mdc-cover-expand:not(.expand) .chapter-feed__cover {bottom: 0px;}
        #__nuxt[mdcpage="follow"][mdccoverenabled="true"][mdccoverexpand="Float Down"] .mdc-cover-expand .chapter-feed__cover {top: 0px;}
      `;

      addGlobalStyle(style);
    }

    // Title / Group.
    {
      const style = `
        /* Remove the spacing and apply chapter line separators. */
        #__nuxt[mdcpage="title"][mdcce="true"] .flex.flex-col.gap-2 {gap: 0rem !important;}
        #__nuxt[mdcpage="title"][mdcce="true"] .chapter {border-bottom: 1px solid rgb(var(--md-accent-30)) !important;}

        /* Remove bolding of chapter titles and adjust the font size, but leave a little bolding for unread. */
        #__nuxt[mdcpage="title"][mdccf="true"] .chapter:not(.read) .chapter-link {font-weight: 500 !important; font-size: 0.75rem !important;}
        #__nuxt[mdcpage="title"][mdccf="true"] .chapter.read .chapter-link {font-weight: normal !important; font-size: 0.75rem !important;}
        #__nuxt[mdcpage="title"][mdccf="true"] .bg-accent.rounded-sm.read .font-bold {font-weight: normal !important;}

        /* Adjust line height of unread chapters. */
        #__nuxt[mdcpage="title"][mdcce="true"] .chapter:not(.read) > div.chapter-grid {line-height: 1.25rem;}
      `;

      addGlobalStyle(style);
    }

    // All.
    {
      const style = `
        /* Settings cog. */
        button.condensed-settings {position: relative;}
        button.condensed-settings::after {background: #000; opacity: 0; content: ""; position: absolute; top: 0; bottom: 0; left: 0; right: 0; transition: all .1s ease-out;}
        button.condensed-settings:hover::after {opacity: 0.2;}

        /* Adjust the font size and styling. */
        #__nuxt[mdccf="true"] .chapter-feed__title {font-size: 0.75rem !important;}
        #__nuxt[mdccf="true"] .chapter-grid {font-size: 0.75rem !important;}
        #__nuxt[mdccf="true"] .chapter-grid .font-bold {font-weight: normal !important;}

        /* Alter the grid spacing to give more room for the chapter name. */
        @media (min-width:48rem) {
          #__nuxt[mdcce="true"] .chapter-grid {grid-template-areas: "title spacer groups uploader views timestamp comments" !important;}
          #__nuxt[mdcce="true"] .chapter-grid {grid-template-columns: auto auto fit-content(100%) fit-content(100%) min-content min-content 6ch !important;}
          #__nuxt[mdcce="true"] .chapter-grid {padding-top: 0.15rem !important; padding-bottom: 0 !important; row-gap: 0.15rem !important;}
        }

        /* Adjust container margin to be smaller. */
        #__nuxt[mdcce="true"] .chapter-feed__container.mb-4 {margin-bottom: 0.5rem !important;}

        /* Adjust the lift color for read chapters. */
        .chapter.read .pill.lift:hover {background-color:rgb(var(--md-accent-10)) !important;}
        .chapter.read .group-tag.lift:hover {background-color:rgb(var(--md-accent-10)) !important;}

        /* Add a lift for comments. */
        .chapter.read [title*="comment"]:hover {background-color:rgb(var(--md-accent-10));}

        /* Identify read chapters easier. */
        /* Darken the background color. */
        .light #__nuxt[mdcstyle="Darken Background"] {--mdc-read-background: var(--md-accent-50);}
        .dark #__nuxt[mdcstyle="Darken Background"] {--mdc-read-background: var(--md-background);}
        #__nuxt[mdcstyle="Darken Background"] .chapter.read {background-color:rgb(var(--mdc-read-background)) !important;}
        #__nuxt[mdcstyle="Darken Background"] .condensed-read {background-color:rgb(var(--mdc-read-background)) !important;}
        #__nuxt[mdcstyle="Darken Background"] .bg-accent.rounded-sm.read {background-color:rgb(var(--md-read-background)) !important;}
        .light #__nuxt[mdcstyle="Darken Background"] .chapter.read {color:#828282 !important;}
        .dark #__nuxt[mdcstyle="Darken Background"] .chapter.read {color:#6a6a6a !important;}

        /* Gray out the chapter name. */
        .light #__nuxt[mdcstyle="Lighten Text"] .chapter.read {color:#b9b9b9 !important;}
        .dark  #__nuxt[mdcstyle="Lighten Text"] .chapter.read {color:#6a6a6a !important;}

        /* Hide. */
        #__nuxt[mdcstyle="Hide"] .chapter.read {display:none !important;}
        #__nuxt[mdcstyle="Hide"] .condensed-read {display:none !important;}
      `;

      addGlobalStyle(style);
    }
  }

  ///////////////////////////////////////////////////////////////////////////////
  function pageFollows() {
    const container_selector = '#__nuxt';
    const config_class = 'controls';

    function style() {
      const coverMode = GM_config.get('CoverMode');
      const coverStyle = GM_config.get('CoverStyle');
      const coverExpand = GM_config.get('CoverExpandDirection');
      const readStyle = GM_config.get('ReadChapterStyle');
      const condenseElements = GM_config.get('CondenseElements');
      const condenseFonts = GM_config.get('CondenseFonts');

      const nuxt = document.getElementById('__nuxt');

      nuxt.setAttribute('mdcpage', 'follow');
      nuxt.setAttribute('mdccover', coverStyle);
      nuxt.setAttribute('mdccoverexpand', coverExpand);
      nuxt.setAttribute('mdcstyle', readStyle);
      if (condenseElements) nuxt.setAttribute('mdcce', condenseElements);
      if (condenseFonts) nuxt.setAttribute('mdccf', condenseFonts);

      if (coverStyle !== 'Hidden' || coverStyle === 'Hidden' && ['Title + Cover', 'Container'].includes(coverMode))
        nuxt.setAttribute('mdccoverenabled', true);
      if (['Float Up', 'Float Down'].includes(coverExpand))
        nuxt.setAttribute('mdccoverfloat', true);
    }

    function observer() {
      const apply_js_cb = async function(mutationsList, observer) {

        let loadedOne = false;
        const containers = document.getElementsByClassName('chapter-feed__container');
        for (const container of containers) {
          const title = container.getElementsByClassName('chapter-feed__title')[0];
          const cover = container.getElementsByClassName('chapter-feed__cover')[0];
          const chapters = container.getElementsByClassName('chapter-feed__chapters')[0];

          if (title && cover && chapters) {
            // Abort if we've already processed this title.
            // Our observer can get called multiple times.
            if (title.classList.contains('condensed-parsed'))
              return;

            // Mark that we loaded at least one thing.
            // We know the page has loaded so we can try to inject the settings cog latter on.
            loadedOne = true;

            const coverMode = GM_config.get('CoverMode');
            const coverStyle = GM_config.get('CoverStyle');
            const coverExpand = GM_config.get('CoverExpandDirection');
            let count = 0;
            let hideTimeout = 0;
            let touchAndMove = false;

            // If we are popping the cover out, add some grace for the hide.
            if (coverStyle === 'Hidden') {
              if (coverMode === 'Title + Cover') hideTimeout = 100;
              if (coverMode === 'Container') hideTimeout = 50;
            }

            const hide = function(e, t = hideTimeout) {
              console.log('[MDC] Hiding cover via ' + e.type);
              touchAndMove = false;

              // Compact mode doesn't show the cover.  Trying to mess with it will break the page.
              if (container.classList.contains('compact')) return;

              setTimeout(() => {
                if (--count <= 0) {
                  count = 0;
                  container.classList.remove('mdc-cover-expand');
                }
              }, t);
            };
            const show = function(e) {
              console.log('[MDC] Showing cover via ' + e.type);

              // Compact mode doesn't show the cover.  Trying to mess with it will break the page.
              if (container.classList.contains('compact')) return;

              ++count;
              container.classList.add('mdc-cover-expand');
            };
            const touchMove = function(e) {
              touchAndMove = true;
            }
            const contextMenu = function(e) {
              if (touchAndMove) {
                e.preventDefault();
                console.log('[MDC] Preventing contextmenu due to touch and hold.');
              } else {
                hide(e);
              }
            }

            // Controls our method of showing covers.
            // Mouse enters: Show the cover and move the chapters over to the next column.
            // Mouse leaves: Hide the cover and span the chapters across the whole grid row.
            if (coverStyle !== 'Full Size') {
              const events = [['mouseenter', show], ['mouseleave', hide], ['touchstart', show], ['touchend', hide], ['touchcancel', hide], ['touchmove', touchMove], ['contextmenu', contextMenu]];
              if (coverMode === 'Container') {
                events.forEach((ev) => container.addEventListener(ev[0], ev[1]));
              }
              if (coverMode === 'Title' || coverMode == 'Title + Cover') {
                events.forEach((ev) => title.addEventListener(ev[0], ev[1]));
              }
              if (coverMode === 'Cover' || coverMode == 'Title + Cover') {
                events.forEach((ev) => cover.addEventListener(ev[0], ev[1]));
              }
            }

            // Adding our event listeners might have triggered a weird browser issue where our mouseenter event got triggered twice.
            // I noticed this happens on Firefox if your mouse is already over a cover image on page load.
            // Set our count to 0 to allow the hide function to properly clean up.
            setTimeout(() => { count = 0; }, 1);

            // Set up default state (cover hidden).
            if (coverStyle === 'Hidden') {
              hide(undefined, 0);
            }

            // Add functionality for each chapter.
            for (const chapter of chapters.querySelectorAll('.chapter')) {

              // Remove events from the child anchor tags.
              // These Vue events cancel the event bubble which prevents our changes from working.
              removeElementEvents(chapter.querySelectorAll('a'));

              // Alter anchor target.
              const leftClickMode = GM_config.get('LeftClickMode');
              if (leftClickMode === 'New Window')
                rebindLeftClick(chapter);

              // Add event to mark the chapter as read when clicked.
              // MangaDex will throw an error if a page navigation happens at the same time so don't bind on click
              // unless we re-target clicks to open in a new window.
              chapter.addEventListener('auxclick', toggleRead);
              if (leftClickMode === 'New Window')
                chapter.addEventListener('click', toggleRead);
            }

            // Remove the alt-text on the flag.
            // This prevents text overlap on the title when MangaDex isn't loading.
            const flag = title.firstElementChild;
            if (flag !== null) flag.alt = '';

            // Mark that we've processed this title.
            title.classList.add('condensed-parsed');
          }
        }

        if (loadedOne) {
          addConfig();
        }
      };

      // Used to prevent spawning a bunch of setTimeouts if we have rapid mutation callbacks.
      let waiting_for_timeout = false;

      // Applies "read" status to the whole manga container if every chapter under it has been read.
      const apply_read_cb = async function(mutationsList, observer) {

        // We disconnect our observer so the changes we make don't cause new mutations.
        if (observer !== undefined) {
          observer.disconnect();
          observer.takeRecords();
        }

        if (mutationsList.some((e) => e.attributeName === 'class')) {

          // Only process if we are waiting for a callback.
          if (!waiting_for_timeout) {
            waiting_for_timeout = true;

            // Set a small timeout so the changes apply before we test for read chapters.
            setTimeout(() => {
              waiting_for_timeout = false;
              const containers = document.getElementsByClassName('chapter-feed__container');

              for (const container of containers) {
                const chapters = container.getElementsByClassName('chapter-feed__chapters')[0];
                if (!chapters)
                  continue;

                const chapter = chapters.getElementsByClassName('chapter');

                // If all chapters for this title have been read, apply the condensed-read class to the container.
                if (Array.prototype.every.call(chapter, (e) => e.classList.contains('read'))) {
                  container.classList.add('condensed-read');
                }
                else {
                  container.classList.remove('condensed-read');
                }
              }

              // Reconnect our observer now that we pushed changes.
              try {
                const page_container = document.querySelector(container_selector);
                observer.observe(page_container, {attributes: true, subtree: true, attributeFilter: ['class']});
              } catch (error) {}
            }, 10);
          }
        }
      };

      try {
        //debugger;
        const page_container = document.querySelector(container_selector);
        const chapter_observer = new MutationObserver(apply_js_cb);
        chapter_observer.observe(page_container, {attributes: false, childList: true, subtree: true});

        const read_observer = new MutationObserver(apply_read_cb);
        read_observer.observe(page_container, {attributes: true, subtree: true, attributeFilter: ['class']});

        current_page_observers.push(chapter_observer, read_observer);

        apply_js_cb();
      } catch (error) {}
    }

    function addConfig() {
      const controls = document.getElementsByClassName(config_class)[0];
      if (controls === undefined || controls.getElementsByClassName('condensed-settings').length !== 0)
        return;

      const divData = findDataAttribute(controls.firstElementChild);
      const svgData = findDataAttribute(controls.firstElementChild.firstElementChild);

      const config = createSettingsButton(divData, svgData);
      config.classList.add('item');
      controls.append(config);
    }

    // Avoid adding tons of duplicate styles.
    if (current_page_observers.length === 0) {
      style();
      observer();
    }
  }

  ///////////////////////////////////////////////////////////////////////////////
  function pageTitle() {
    const container_selector = '#__nuxt';
    const config_class = '.layout-container div.sm\\:ml-2 .flex.mb-6';
    const config_class2 = '.layout-container div.sm\\:ml-2 .flex.mb-2';

    function style() {
      const coverStyle = GM_config.get('CoverStyle');
      const readStyle = GM_config.get('ReadChapterStyle');
      const condenseElements = GM_config.get('CondenseElements');
      const condenseFonts = GM_config.get('CondenseFonts');

      const nuxt = document.getElementById('__nuxt');

      nuxt.setAttribute('mdcpage', 'title');
      nuxt.setAttribute('mdccover', coverStyle);
      nuxt.setAttribute('mdcstyle', readStyle);
      if (condenseElements) nuxt.setAttribute('mdcce', condenseElements);
      if (condenseFonts) nuxt.setAttribute('mdccf', condenseFonts);
    }

    function observer() {
      const apply_js_cb = function(mutationsList, observer) {

        // Try to add our settings button.
        addConfig();

        const chapters = document.querySelectorAll('.chapter');
        for (const chapter of chapters) {
          // Abort if we've already processed this chapter.
          // Our observer can get called multiple times.
          if (chapter.classList.contains('condensed-parsed'))
            return;

          // Mark that we've processed this chapter.
          chapter.classList.add('condensed-parsed');

          // Put the "read" class on chapter group titles so we can gray out the group text.
          const read = chapter.classList.contains('read');
          if (read) {
            // Check if we are in a group.  We can test this by going to the parent and checking if we have a sibling (the title).
            const is_group = chapter.parentElement.previousElementSibling !== null;
            if (is_group) {
              chapter.parentElement.parentElement.classList.add('read');
            }
          }

          // Remove events from the child anchor tags.
          // These Vue events cancel the event bubble which prevents our changes from working.
          removeElementEvents(chapter.querySelectorAll('a'));

          // Alter anchor target.
          const leftClickMode = GM_config.get('LeftClickMode');
          if (leftClickMode === 'New Window')
            rebindLeftClick(chapter);

          // Add event to mark the chapter as read when clicked.
          // MangaDex will throw an error if a page navigation happens at the same time so don't bind on click
          // unless we re-target clicks to open in a new window.
          chapter.addEventListener('auxclick', toggleRead);
          if (leftClickMode === 'New Window')
            chapter.addEventListener('click', toggleRead);
        }
      };

      try {
        //debugger;
        const page_container = document.querySelector(container_selector);
        const chapter_observer = new MutationObserver(apply_js_cb);
        chapter_observer.observe(page_container, {attributes: false, childList: true, subtree: true});

        current_page_observers.push(chapter_observer);
      } catch (error) {}
    }

    function addConfig() {
      let controls = document.getElementsByClassName('controls')[0];
      if (controls === undefined)
        controls = document.querySelector(config_class);
      if (controls === null)
        controls = document.querySelector(config_class2);
      if (controls === undefined || controls === null || controls.getElementsByClassName('condensed-settings').length !== 0)
        return;

      // Abort if we've already added our control.
      if (controls.classList.contains('condensed-parsed'))
        return;

      // Mark that we've added this control.
      controls.classList.add('condensed-parsed');

      const config = createSettingsButton();
      config.classList.add('rounded', 'relative', 'md-btn', 'flex', 'items-center', 'overflow-hidden', 'px-3', 'justify-center', 'text-black',
                           'dark:text-white', 'bg-accent', 'hover:bg-accent-darken', 'active:bg-accent-darken2',
                           'dark:bg-accent-lighten2', 'dark:hover:bg-accent-lighten', 'dark:active:bg-accent', 'px-0');
      config.style.minHeight = '48px';
      config.style.minWidth = '48px';
      controls.append(config);
    }

    if (current_page_observers.length === 0) {
      style();
      observer();
      addConfig();
    }
  }

  ///////////////////////////////////////////////////////////////////////////////

  ///////////////////////////////////////////////////////////////////////////////
  // This is our loader.
  //debugger;
  const pageContentSelector = '#__nuxt';
  const bootstrap_loader = function(mutationsList, observer) {
    console.log('[MDC] Bootstrap loader.');
    observer.disconnect();
    observer.takeRecords();

    // Detects page changes.
    const page_transfer_loader = function(mutationsList, observer) {

      const full_location = location.pathname + location.search;
      if (previous_pathname === full_location)
        return;

      previous_pathname = full_location;
      if (current_page_observers.length !== 0) {
        current_page_observers.forEach((x) => { x.disconnect(); x.takeRecords(); });
        current_page_observers = [];
      }

      // Choose the style function to apply.
      const follows = [/\/titles\/feed/, /\/titles\/latest/, /\/my\/history/, /\/user\//, /\/group\//];
      const title = [/\/title\//];
      let pageFunction = undefined;
      if (follows.filter((url) => url.test(full_location)).length > 0)
        pageFunction = pageFollows;
      else if (title.filter((url) => url.test(full_location)).length > 0)
        pageFunction = pageTitle;

      if (pageFunction !== undefined) {
        console.log(`[MDC] Page detected, calling ${pageFunction.name}.`);
        pageFunction();
      }

      // observer.observe(content_container, {attributes: false, childList: true, subtree: true});
    };

    const content_container = document.querySelector(pageContentSelector);
    const content_observer = new MutationObserver(page_transfer_loader);
    content_observer.observe(content_container, {attributes: false, childList: true, subtree: true});

    // Test for the page already being loaded.  This is a race condition that could break the observer.
    if (content_container.hasChildNodes()) {
      console.log('[MDC] Page loaded, jumping to page detection.');
      page_transfer_loader([], content_observer);
    }
  };

  // This is the first bootstrap loader.
  // This will catch the main page being loaded.
  // At this point, we switch over to our page transfer loader which will detect page changes.
  window.addEventListener('load', (event) => {
    // Apply the styles now.  They will sit for all future pages.
    addStyles();

    const load_observer = new MutationObserver(bootstrap_loader);
    load_observer.observe(document.body, {attributes: false, childList: true, subtree: false});

    // Test for the page already being loaded.  This is a race condition that could break the observer.
    //debugger;
    const content_container = document.querySelector(pageContentSelector);
    if (content_container != null && content_container.hasChildNodes()) {
      console.log('[MDC] Page loaded, jumping to bootstrap.');
      bootstrap_loader([], load_observer);
    }
  });
})();