Internet Roadtrip - Vote History

Show the result of the last 8 votes in neal.fun/internet-roadtrip

// ==UserScript==
// @name        Internet Roadtrip - Vote History
// @description Show the result of the last 8 votes in neal.fun/internet-roadtrip
// @namespace   me.netux.site/user-scripts/internet-roadtrip/vote-history
// @version     1.8
// @author      Netux
// @license     MIT
// @match       https://neal.fun/internet-roadtrip/*
// @icon        https://neal.fun/favicons/internet-roadtrip.png
// @grant       none
// ==/UserScript==

(() => {
  const MAX_ENTRIES = 8;

  const containerEl = document.querySelector('.container');

  const state = {
    dom: {}
  };

  function start(vue) {
    state.vue = vue;

    setupDom();
    patch(vue);

    /*
    for (let i = 0; i < 5; i++) {
      const vote = -2;
      addVote(vote, 0, { [vote]: { heading: 180 } }, { [vote]: 100, [9999]: 50 });
    }
    */
  }

  function setupDom() {
    injectStylesheet();

    state.dom.voteHistoryContainerEl = document.createElement('ul');
    state.dom.voteHistoryContainerEl.className = 'vote-history';
    containerEl.appendChild(state.dom.voteHistoryContainerEl);
  }

  function injectStylesheet() {
    const styleEl = document.createElement('style');

    styleEl.textContent = `
    .vote-history {
      position: fixed;
      left: 10px;
      top: 150px;
      margin: 0;
      padding: 0;
      list-style: none;
      color: white;
      font-family: "Roboto", sans-serif;
      font-size: 0.8rem;
      pointer-events: none;

      & .vote-history-entry {
        margin: 0.5rem 0;
        text-shadow: 1px 1px 2px black;

        & .vote-history-entry__icon-container {
          position: relative;
          height: 12px;
          aspect-ratio: 1;
          margin-right: 0.5rem;
          vertical-align: middle;
          display: inline-block;

          & > img {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
          }

          & .vote-history-entry__icon-shadow {
            opacity: .5;
            scale: 1.5;
            filter: blur(5px);
          }

          & .vote-history-entry__icon {
            filter: invert(1);
            z-index: 1;
          }
        }

        & .vote-history-entry__time {
          margin-left: 1ch;
          font-size: 80%;
          color: lightgrey;
        }

        ${new Array(MAX_ENTRIES).fill(null).map((_, i) => `
        &:nth-child(${i + 1}) {
          opacity: ${1.2 - Math.pow(1 - ((MAX_ENTRIES - i) / MAX_ENTRIES), 2)};
        }
        `).join('\n\n')}
      }
    }
    `;

    document.head.appendChild(styleEl);
  }

  function patch(vue) {
    const ogChangeStop = vue.changeStop;
    vue.changeStop = function (_, currentChosen) {
      if (!this.isChangingStop) {
        try {
          addVote(currentChosen, this.currentHeading, this.currentOptions, this.voteCounts);
        } catch (error) {
          console.error('Could not add vote:', { currentChosen, currentHeading: this.currentHeading, currentOptions: this.currentOptions, voteCounts: this.voteCounts }, error);
        }
      }

      return ogChangeStop.apply(this, arguments);
    }
  }

  function addVote(vote, heading, options, voteCounts) {
    function angleDifference(a, b) {
      var t = (b - a) % 360;
      if (t > 180) t -= 360;
      if (t < -180) t += 360;
      return t;
    }
    function getRotation(vote) {
      return 0.8 * angleDifference(heading, options[vote].heading);
    }

    const newEntryEl = document.createElement('li');
    newEntryEl.className = 'vote-history-entry';

    let entryActionText = '?';
    let entryIconSrc = null;
    let entryIconRotation = 0;
    switch (vote) {
      case -2: {
        entryActionText = "HONK!";
        entryIconSrc = '/internet-roadtrip/honk.svg'
        break;
      }
      case -1: {
        entryActionText = "Seek Radio";
        entryIconSrc = '/internet-roadtrip/skip.svg'
        break;
      }
      default: {
        entryIconSrc = '/internet-roadtrip/chevron-black.svg';
        if (options[vote]) {
          entryActionText = options[vote].description;
          entryIconRotation = getRotation(vote);
        } else {
          entryActionText = 'Turn Around';
          entryIconRotation = 180;
        }
        break;
      }
    }

    const voteIconEl = document.createElement('div');
    voteIconEl.className = 'vote-history-entry__icon-container';
    if (entryIconRotation !== 0) {
      voteIconEl.style.rotate = `${entryIconRotation}deg`;
    }
    newEntryEl.appendChild(voteIconEl);

    const voteIconShadowImageEl = document.createElement('img');
    voteIconShadowImageEl.className = 'vote-history-entry__icon-shadow';
    voteIconShadowImageEl.src = entryIconSrc;
    voteIconEl.appendChild(voteIconShadowImageEl);

    const voteIconImageEl = document.createElement('img');
    voteIconImageEl.className = 'vote-history-entry__icon';
    voteIconImageEl.src = entryIconSrc;
    voteIconEl.appendChild(voteIconImageEl);

    const voteCount = voteCounts[vote];
    const entryVotesText = voteCount != null
      ? `${voteCount} vote${voteCount === 1 ? '' : 's'}, ${Math.round(voteCount / Object.values(voteCounts).reduce((acc, votes) => acc + votes, 0) * 100)}%`
      : 'no votes';
    const entryTextNode = document.createTextNode(`${entryActionText} (${entryVotesText})`);
    newEntryEl.appendChild(entryTextNode);

    const entryTimeEl = document.createElement('span');
    entryTimeEl.className = 'vote-history-entry__time';
    entryTimeEl.innerText = new Date().toLocaleTimeString();
    newEntryEl.appendChild(entryTimeEl);

    while (state.dom.voteHistoryContainerEl.childElementCount >= MAX_ENTRIES) {
      state.dom.voteHistoryContainerEl.lastElementChild.remove();
    }

    if (state.dom.voteHistoryContainerEl.childElementCount > 0) {
      state.dom.voteHistoryContainerEl.insertBefore(newEntryEl, state.dom.voteHistoryContainerEl.firstChild);
    } else {
      state.dom.voteHistoryContainerEl.appendChild(newEntryEl);
    }
  }

  if (typeof unsafeWindow !== 'undefined') {
    unsafeWindow.DEBUG__addVoteToHistory = addVote;
  }


  {
    const waitForVueInterval = setInterval(() => {
      const vue = containerEl.__vue__;
      if (!vue?.changeStop) {
        return;
      }

      clearInterval(waitForVueInterval);
      start(vue);
    }, 100);
  }
})();