WK Read that if you can

Change font and font sizing for wanikani reviews

// ==UserScript==
// @name         WK Read that if you can
// @namespace    http://tampermonkey.net/
// @version      0.2
// @description  Change font and font sizing for wanikani reviews
// @author       Gorbit99
// @include      /^https:\/\/(preview\.|www\.)wanikani.com\/(review|extra_study|lesson)\/session.*$/
// @icon         https://www.google.com/s2/favicons?sz=64&domain=wanikani.com
// @grant        none
// @require      https://greasyfork.org/scripts/441792-cidwwa/code/CIDWWA.js?version=1031511
// @license      MIT
// @run-at       document-body
// ==/UserScript==
"use strict";

(function() {
  const googleApiLink = document.createElement("link");
  googleApiLink.rel = "preconnect";
  googleApiLink.href = "https://fonts.googleapis.com";
  const gstaticLink = document.createElement("link");
  gstaticLink.rel = "preconnect";
  gstaticLink.href = "https://fonts.gstatic.com";
  gstaticLink.crossOrigin = true;
  document.head.append(googleApiLink);
  document.head.append(gstaticLink);

  function addFontToSite(font) {
    const link = document.createElement("link");
    link.href =
      "https://fonts.googleapis.com/css?family=" +
      font.family.replace(/ /g, "+") +
      "&subset=japanese";
    link.rel = "stylesheet";
    document.head.append(link);
  }

  let settings = {
    googleFonts: {},
    fontMin: 15,
    fontMax: 60,
  };

  const loadedSettings = window.localStorage.getItem("read-that.settings");

  const fontCache = JSON.parse(
    window.localStorage.getItem("read-that.cache") ?? "{}"
  );

  for (let key in fontCache) {
    addFontToSite(fontCache[key]);
  }

  if (loadedSettings) {
    settings = JSON.parse(loadedSettings);
  }

  function saveSettings() {
    window.localStorage.setItem("read-that.settings", JSON.stringify(settings));
  }

  const modal = window.createModal({
    title: "Read That Settings",
  });

  function populateModal() {
    const containerStyle = `
      max-height:60vh;
      overflow-y:auto;
      width:1000px;
    `;

    const googleFontsStyle = `
      overflow-y:auto;
      border:1px solid grey;
      max-height:20em;
      min-height:10em;
      padding:0.5rem;
    `;

    const fontSizeStyle = `
      display:flex;
      justify-content:center;
      gap:2em;
      align-items:end;
    `;

    const onlineFontsStyle = `
      overflow-y:auto;
      border:1px solid grey;
      max-height:20em;
      min-height:10em;
      padding:0.5rem;
    `;

    modal.setContent(`
      <div class="read-that-settings" style="${containerStyle}">
        <h3>Font Size</h3>
        <div style="${fontSizeStyle}">
          <input type="number" class="read-that-font-min" 
            value="${settings.fontMin}" style="width:5ch"/>
          <span class="read-that-font-min-example" 
            style="font-size:${settings.fontMin}px">小</span>
          <span class="read-that-font-max-example" 
            style="font-size:${settings.fontMax}px">大</span>
          <input type="number" class="read-that-font-max" 
            value="${settings.fontMax}" style="width:5ch"/>
        </div>
        <h3>Google Fonts</h3>
        <div class="read-that-google-fonts" style="${googleFontsStyle}"></div>
      </div>
    `);

    loadGoogleFonts().then();

    const fontMin = document.querySelector(".read-that-font-min");
    const fontMax = document.querySelector(".read-that-font-max");

    modal.onClose(() => {
      [...document.querySelectorAll(".read-that-google-selector")]
        .forEach((selector) => {
          settings.googleFonts[selector.name] = selector.checked;
        });
      settings.fontMin = parseInt(fontMin.value);
      settings.fontMax = parseInt(fontMax.value);
      saveSettings();
      modal.close();
    });

    const fontMinExample =
      document.querySelector(".read-that-font-min-example");
    const fontMaxExample =
      document.querySelector(".read-that-font-max-example");

    fontMin.addEventListener("keydown", (e) => {
      e.stopPropagation();
    });

    fontMax.addEventListener("keydown", (e) => {
      e.stopPropagation();
    });

    fontMin.addEventListener("mouseup", (e) => {
      e.stopPropagation();
    });

    fontMax.addEventListener("mouseup", (e) => {
      e.stopPropagation();
    });

    fontMin.addEventListener("change", () => {
      let minVal = parseInt(fontMin.value);
      let maxVal = parseInt(fontMax.value);

      if (minVal > 300) {
        fontMin.value = 300;
        minVal = 300;
      }
      if (minVal < 10) {
        fontMin.value = 10;
        minVal = 10;
      }
      if (maxVal < minVal) {
        fontMax.value = minVal;
        fontMaxExample.style.fontSize = `${fontMax.value}px`;
      }
      fontMinExample.style.fontSize = `${fontMin.value}px`;
    });

    fontMax.addEventListener("change", () => {
      let minVal = parseInt(fontMin.value);
      let maxVal = parseInt(fontMax.value);

      if (maxVal > 300) {
        fontMax.value = 300;
        maxVal = 300;
      }
      if (maxVal < 10) {
        fontMax.value = 10;
        maxVal = 10;
      }
      if (maxVal < minVal) {
        fontMin.value = maxVal;
        fontMinExample.style.fontSize = `${fontMin.value}px`;
      }
      fontMaxExample.style.fontSize = `${fontMax.value}px`;
    });
  }

  async function loadGoogleFonts() {
    const fonts = (await (
      await fetch(
        window.atob("aHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy"
          + "5jb20vd2ViZm9udHMvdjEvd2ViZm9udHM/a2V5PQ==") + stylingString)
    ).json())
      .items
      .filter((font) => font.subsets.includes("japanese"));

    const googleFontsContainer =
      document.querySelector(".read-that-google-fonts");

    fonts.forEach((font) => {
      if (fontCache[font.family] === undefined) {
        fontCache[font.family] = font;
        addFontToSite(font);
      }
    });

    window.localStorage.setItem("read-that.cache", JSON.stringify(fontCache));

    const formContainerStyle = `
      display:flex;
      flex-direction:row;
      align-items:center;
    `;

    const fontExampleStyle = `
      text-overflow:ellipsis;
      white-space:nowrap;
      overflow:hidden;
    `;

    googleFontsContainer.innerHTML = fonts.map((font, i) => `
        <div class="control-group flag"
          style="
            ${formContainerStyle}background:${i % 2 == 0 ? " #fff" : "#eee"};
          ">
        <div style="${fontExampleStyle}">
          <label class="flag__item" style="display:block;">
            ${font.family}
          </label>
          <span style="font-family:'${font.family}'; font-size:2em;">
            ${exampleSentence}
          </span>
        </div>
        <div
          style="
        flex-grow:1; display:flex; justify-content:center; width:5em;
      ">
          <input
            type="checkbox"
            ${settings.googleFonts[font.family] ? "checked" : ""}
            class="read-that-google-selector"
            name="${font.family}"
            style="width:20px; height:20px;" />
        </div>
      </div>
    `).join("");
  }

  const exampleSentence =
    "政治家のキャリア40年にしてようやく総理大臣にまで登りつめました。";

  function addSettingsButton() {
    const buttonContainer = document.querySelector("#summary-button");
    const newButton = document.createElement("a");
    buttonContainer.append(newButton);
    newButton.innerHTML = `
      <i class="fa fa-font"></i>
    `;
    newButton.style.cursor = "pointer";
    newButton.title = "Open font settings";
    newButton.addEventListener("click", () => modal.toggle());
  }

  const stylingString = "AIzaSyAhd-3ke-7i5MBSJGF4HdLffjqhGvggJvo&sort=alpha";
  function setUpChanger() {
    const styleElem = document.createElement("style");
    document.head.append(styleElem);

    const callback = () => {
      const loadedFonts = Object.entries(settings.googleFonts)
        .filter((font) => font[1]).map((font) => font[0]);
      const randomFont =
        loadedFonts[Math.floor(Math.random() * loadedFonts.length)];

      const deltaSize = settings.fontMax - settings.fontMin + 1;
      const offset = Math.floor(Math.random() * deltaSize);
      const fontSize = settings.fontMin + offset;
      styleElem.innerHTML = `
        #character {
          display: flex;
          justify-content: center;
          align-items: center;
          height: ${Math.max(settings.fontMax * 1.5, 330)}px;
        }
        
        #character:not(:hover) span:not(.shown) {
          font-family:${randomFont};
          font-size:${fontSize}px;
        }

        #character span {
          white-space: nowrap;
        }
      `;

      const textElem = document.querySelector("#character span");
      const scaleRatio =
        Math.min(document.body.clientWidth / textElem.scrollWidth, 0.95);
      textElem.style.transform = `scale(${scaleRatio})`;
    };

    callback();

    window.$.jStorage.listenKeyChange("currentItem", callback);

    window.addEventListener("keydown", (e) => {
      if (e.key !== "Alt") {
        return;
      }
      document.querySelector("#character span").classList.add("shown");
    });

    window.addEventListener("keyup", (e) => {
      if (e.key !== "Alt") {
        return;
      }
      document.querySelector("#character span").classList.remove("shown");
    });
  }

  function wireUpOnlineFonts() {
    const onlineFontsContainer =
      document.querySelector(".read-that-online-fonts");

    const onlineFontsStyleElem = document.createElement("style");
    onlineFontsStyleElem.innerHTML = `
      .read-that-of-container.read-that-of-state {
      padding:1rem;
      border-radius:50 %;
      position:relative;
      height:0;
    }

    .read-that-of-container.read-that-of-state:before {
      font-weight:900;
      font-size:1em;
      font-family:FontAwesome;
      position:absolute;
      top:50 %;
      left:50 %;
      transform:translate(-50 %, -50 %);
    }

    .read-that-of-container.empty.read-that-of-state {
      background:lightgrey;
    }

    .read-that-of-container.empty.read-that-of-state:before {
      content:"\\3f";
    }

    @keyframes rotation {
      from {
        transform:rotate(0deg) translate(-50 %, -50 %);
      }
      to {
        transform:rotate(360deg) translate(-50 %, -50 %);
      }
    }

    .read-that-of-container.loading.read-that-of-state {
      background:yellow;
    }

    .read-that-of-container.loading.read-that-of-state:before {
      content:"\\f110";
      animation:rotation 1s steps(8) infinite;
      transform-origin:0 % 0 %;
    }

    .read-that-of-container.correct.read-that-of-state {
      background:darkgreen;
    }

    .read-that-of-container.correct.read-that-of-state:before {
      content:"\\f00c";
      color:white;
    }

    .read-that-of-container.incorrect.read-that-of-state {
      background:red;
    }

    .read-that-of-container.incorrect.read-that-of-state:before {
      content:"\\f071";
      color:white;
    }
    `;

    document.head.append(onlineFontsStyleElem);

    let nextElement;

    const addField = () => {
      const fieldContainer = document.createElement("div");

      onlineFontsContainer.append(fieldContainer);

      fieldContainer.innerHTML = `
        <input class="read-that-of-input" style="width:90%; padding:0.5em;">
        <span class="read-that-of-state"></span>
    `;

      fieldContainer.classList.add("read-that-of-container");
      fieldContainer.classList.add("empty");
      fieldContainer.style.display = "flex";
      fieldContainer.style.flexDirection = "row";
      fieldContainer.style.justifyContent = "space-between";
      fieldContainer.style.margin = "0.25em";
      fieldContainer.style.alignItems = "center";

      const inputField = fieldContainer.querySelector(".read-that-of-input");

      const checkFont = async () => {
        fieldContainer.classList.replace("empty", "loading");

        const font = new FontFace("test", `url(${inputField.value}`);
        font.load().then(() => {
          fieldContainer.classList.replace("loading", "correct");
        }).catch((e) => {
          fieldContainer.classList.replace("loading", "incorrect");
        });
      };

      inputField.addEventListener("blur", () => {
        if (inputField.value.length !== 0) {
          checkFont();
          return;
        }
        if (onlineFontsContainer.childElementCount !== 1) {
          fieldContainer.remove();
        }
      });

      inputField.addEventListener("focus", () => {
        if (nextElement.container === fieldContainer) {
          addField();
        }
      });

      inputField.addEventListener("keydown", (e) => {
        e.stopPropagation();
      });

      nextElement = {
        input: inputField,
        container: fieldContainer,
      };
    };

    addField();
  }

  populateModal();
  addSettingsButton();
  setUpChanger();
})();