Internet Roadtrip - Combined Votes Counts UI

Moves the vote counts in neal.fun/internet-roadtrip from the top right panel to be alongside the arrows, on the wheel, and in the radio

// ==UserScript==
// @name        Internet Roadtrip - Combined Votes Counts UI
// @description Moves the vote counts in neal.fun/internet-roadtrip from the top right panel to be alongside the arrows, on the wheel, and in the radio
// @namespace   me.netux.site/user-scripts/internet-roadtrip/combined-votes-counts-ui
// @version     1.2.1
// @author      Netux
// @license     MIT
// @match       https://neal.fun/internet-roadtrip/*
// @icon        https://neal.fun/favicons/internet-roadtrip.png
// @run-at      document-end
// @grant       GM.getValue
// @grant       GM.setValue
// @require     https://cdn.jsdelivr.net/npm/[email protected]
// ==/UserScript==

/* globals IRF */

(async () => {
  {
    const styleEl = document.createElement('style');
    styleEl.innerText = `
    .container {
      & .results {
        top: 50px;
        right: 10px;
        width: fit-content;
        min-width: 200px;
        padding: 7px 10px;

        &::after {
          /* annoying... */
          pointer-events: none;
        }

        & .results-content {
          padding-bottom: 6px;
          display: none;
        }

        & .results-content-toggle-button {
          width: 100%;
          height: 0.6rem;
          margin-block: 0.3rem 0.1rem;
          background-image: url("https://www.svgrepo.com/show/257732/up-arrow.svg");
          background-size: contain;
          background-position: center;
          background-repeat: no-repeat;
          cursor: pointer;
          display: block;
        }

        &.results-content-open {
          & .results-content-toggle-button {
            rotate: 180deg;
          }

          & .results-content {
            display: revert;
          }
        }
      }

      & .options {
        & .option-votes {
          position: absolute;
          font-family: "Roboto", sans-serif;
          font-size: 12px;
          color: white;
          text-shadow: 1px 1px 2px black;
          bottom: -0.4em;
          left: 50%;
          translate: -50% 0;
          pointer-events: none;
          white-space: nowrap;
        }
      }

      & .wheel-container {
        & .wheel-honk-votes {
          position: absolute;
          top: 22%;
          left: 50%;
          translate: -50%;
          font-family: "Roboto", sans-serif;
          font-size: 20px;
          color: white;
          text-shadow: 1px 1px 2px black;
          white-space: nowrap;
          pointer-events: none;
        }
      }
    }

    @media screen and (max-width: 900px) {
      .container {
        & .results {
          top: 41px;
          right: 5px;
        }
      }
    }
    `;
    document.head.appendChild(styleEl);
  }

  const containerVDOM = await IRF.vdom.container;
  const resultsEl = await IRF.dom.results;
  const resultsVDOM = await IRF.vdom.results;
  const optionsContainerEl = await IRF.dom.options;
  const wheelContainerEl = await IRF.dom.wheel;
  const radioEl = await IRF.dom.radio;

  const wheelHonkVotesEl = document.createElement('span');
  const radioSeekVotesTextNode = document.createTextNode('0');

  function ensureOptionVotesEl(optionEl) {
    let votesEl = optionEl._votesEl;
    if (!votesEl) {
      votesEl = document.createElement('span');
      votesEl.className = 'option-votes';
      votesEl.textContent = `0 (0%)`;
      optionEl.appendChild(votesEl);
      optionEl._votesEl = votesEl;
    }

    return votesEl;
  }

  function updateVotes(votes) {
    const totalVotes = Object.values(votes).reduce((total, count) => total + count, 0);

    const optionEls = optionsContainerEl.querySelectorAll('.option');

    for (const [voteStr, votesCount] of Object.entries(votes)) {
      const percentageStr = `${totalVotes === 0 ? 0 : Math.floor(votesCount / totalVotes * 100)}%`;

      switch (voteStr) {
        case "-2": {
          wheelHonkVotesEl.textContent = `${votesCount} (${percentageStr})`;
          break;
        }
        case "-1": {
          radioSeekVotesTextNode.textContent = votesCount;
          break;
        }
        default: {
          const voteIndex = parseInt(voteStr, 10);

          const optionEl = optionEls[voteIndex];
          if (!optionEl) {
            continue;
          }

          const votesEl = ensureOptionVotesEl(optionEl);

          votesEl.textContent = `${votesCount} (${percentageStr})`;
        }
      }
    }
  }

  {
    const { set: voteCountsSetter } = Object.getOwnPropertyDescriptor(resultsVDOM.state._props, 'voteCounts');
    Object.defineProperty(resultsVDOM.state._props, 'voteCounts', {
      set(newVoteCounts) {
        updateVotes(newVoteCounts);

        return voteCountsSetter.call(this, newVoteCounts);
      },
      configurable: true,
      enumerable: true,
    });
  }

  {
    const optionsContainerMutationObserver = new MutationObserver((records) => {
      for (const record of records) {
        if (record.type !== "childList") {
          continue;
        }

        for (const addedOptionEl of record.addedNodes) {
          if (addedOptionEl.className !== 'option') {
            continue;
          }

          ensureOptionVotesEl(addedOptionEl);
        }
      }
    });
    optionsContainerMutationObserver.observe(optionsContainerEl, {
      childList: true
    });

    const wheelClickArealEl = wheelContainerEl.querySelector('.wheel-click-area');
    wheelHonkVotesEl.className = 'wheel-honk-votes';
    wheelClickArealEl.appendChild(wheelHonkVotesEl);

    const radioSeekButtonLabelEl = radioEl.querySelector('.control-button .button-label');
    radioSeekButtonLabelEl.append(
      document.createTextNode(' ('),
      radioSeekVotesTextNode,
      document.createTextNode(')'),
    );

    const resultsContentToggleEl = document.createElement('div');
    resultsContentToggleEl.className = 'results-content-toggle-button';
    resultsContentToggleEl.addEventListener('click', async () => {
      resultsEl.classList.toggle('results-content-open');
      await GM.setValue('results-content-open', resultsEl.classList.contains('results-content-open'));
    });

    if (await GM.getValue('results-content-open')) {
      resultsEl.classList.toggle('results-content-open', true);
    }

    const resultsContentEl = resultsEl.querySelector('.results-content');
    resultsContentEl.insertAdjacentElement('afterend', resultsContentToggleEl);
  }
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址