auralewd / Single column, big image view

// ==UserScript==
// @name         Single column, big image view
// @namespace    http://tampermonkey.net/
// @version      1.3
// @license      MIT
// @description  Displays roulettes in a single adjustable column, using the full size image.
// @author       Auralewd
// @match        https://faproulette.co/*
// @grant        none
// @run-at       document-end
// ==/UserScript==

(function () {
  "use strict";

  // Map data-file-ext values to file extensions
  const EXT_MAP = {
    "0": "jpg",
    "1": "jpg",
    "2": "png",
    "3": "webp",
    "6": "webp"
  };

  const style = document.createElement("style");
  style.textContent = `

    div.roulette-grid {
        display: block !important;
    }
    div.roulette-container {
        grid-row-end: auto !important;
        height: auto !important;
    }
    .max-width{
        margin-left: auto !important;
        margin-right: auto !important;
        max-width: 95vw !important;
    }
    div.roulettes a.roulette {
        margin-bottom: 30px !important;
        max-height: unset !important;
    }
    /* == Container – transparent, flex layout == */
    #column-edge-slider {
        background: transparent !important;
        width: 100%;
        height: 30px;
        display: flex;
        align-items: center;
        padding: 0 -9px;
        margin: 0;
        box-sizing: border-box;
    }

    /* == Both sliders grow equally, spacer has fixed width == */
    #column-edge-slider input[type="range"] {
        flex: 1 1 0;                /* take remaining space */
        -webkit-appearance: none;
        appearance: none;
        background: transparent;
        height: 100%;
        cursor: pointer;
        margin: 0;
    }

    /* == The 4vw spacer in the middle == */
    #column-edge-slider .spacer {
        flex: 0 0 10vw;              /* fixed 4vw – never grows/shrinks */
        height: 6px;                /* same as track height */
        background: #1f1737;        /* light purple, matching track */
        border-radius: 3px;
    }

    /* == Track (sliding groove) == */
    #column-edge-slider input[type="range"]::-webkit-slider-runnable-track {
        height: 6px;
        background: #292145;
        border-radius: 3px;
    }
    #column-edge-slider input[type="range"]::-moz-range-track {
        height: 6px;
        background: #292145;
        border-radius: 3px;
        border: none;
    }

    /* == Thumb (handle) == */
    #column-edge-slider input[type="range"]::-webkit-slider-thumb {
        -webkit-appearance: none;
        appearance: none;
        width: 18px; height: 18px;
        border-radius: 50%;
        background: white;
        border: 2px solid #302747;
        margin-top: -6px;
        margin-left: -9px;
        margin-right: -9px;
        cursor: pointer;
    }
    #column-edge-slider input[type="range"]::-moz-range-thumb {
        width: 18px; height: 18px;
        border-radius: 50%;
        background: white;
        border: 2px solid #302747;
        margin-left: -9px;
        margin-right: -9px;
        cursor: pointer;
    }

    /* The right slider remains flipped */
    #right-edge {
        transform: scaleX(-1);
    }
  `;
  document.head.appendChild(style);

  function addColumnMarginSlider(insertAfterSelector, containerSelector) {
      // Create slider bar
      const bar = document.createElement('div');
      bar.id = 'column-edge-slider';
      bar.innerHTML = `
          <input type="range" id="left-edge"  min="0" max="45" value="0" step="0.1">
          <div class="spacer"></div>
          <input type="range" id="right-edge" min="0" max="45" value="0" step="0.1">
      `;
      let attempts = 4;

      // Wait for container and init
      function init() {
          const container = document.querySelector(containerSelector);
          attempts = attempts - 1;
          if (!container) {
            if (attempts > 1) {
              return setTimeout(init, 300);
            } else {
              return;
            }
          }
          document.querySelector(insertAfterSelector)?.insertAdjacentElement('afterend', bar) || document.body.appendChild(bar);
          const leftSlider = document.getElementById('left-edge');
          const rightSlider = document.getElementById('right-edge');

          // Load saved margins
          const savedLeft = localStorage.getItem('columnLeftMargin') || 0;
          const savedRight = localStorage.getItem('columnRightMargin') || 0;
          leftSlider.value = savedLeft;
          rightSlider.value = savedRight;

          // Apply margin and save on change
          function update() {
              const left = leftSlider.value;
              const right = rightSlider.value;
              container.style.marginLeft = left + '%';
              container.style.marginRight = right + '%';
              container.style.maxWidth = 'none';
              container.style.width = 'auto';
              localStorage.setItem('columnLeftMargin', left);
              localStorage.setItem('columnRightMargin', right);
          }
          leftSlider.addEventListener('input', update);
          rightSlider.addEventListener('input', update);
          update(); // initial apply
      }
      init();
  }
  addColumnMarginSlider('.grid-header', '.roulette-grid');


  // Remove -thumbs-larger and add correct extension
  function getFullSizeUrl(img) {
    const src = img.src;
    if (!src || !src.includes("-thumbs-larger")) return img.src;

    const parentA = img.closest("a");
    if (!parentA) {
      console.warn("No parent <a> element found for image:", img.src);
      return img.src;
    }

    const extCode = parentA.getAttribute("data-file-ext");
    if (!extCode) {
      console.warn("No data-file-ext attribute found on parent <a>:", img.src);
      return img.src;
    }

    const extension = EXT_MAP[extCode];
    if (!extension) {
      console.error("Unknown data-file-ext value:", extCode);
      alert(`Unknown image extension encountered: ${extCode}. Image URL: ${img.src}`);
      img.src = "";
      return "";
    }

    // Get base URL without -thumbs-larger and without extension
    const baseSrc = img.src.replace(/-thumbs-larger\//, "/");

    // Remove any existing extension
    const urlWithoutExt = baseSrc.replace(/\.\w+$/, "");

    // Add correct extension
    return urlWithoutExt + "." + extension;
  }


  // Check if element is visible
  function isVisible(elem) {
    const style = window.getComputedStyle(elem);
    if (style.display === "none" || style.visibility === "hidden") return false;
    const rect = elem.getBoundingClientRect();
    return rect.width > 0 && rect.height > 0;
  }


  // Process a single image: rewrite URL with correct extension and set up upscaling
  function processImage(img) {
    if (!isVisible(img)) return;

    const src = img.src;
    if (!src) return;

    // Get full-size URL with correct extension
    const fullUrl = getFullSizeUrl(img);
    if (!fullUrl) return;

    // Cancel current load and set new URL
    if (fullUrl != src) {
      img.src = "";
      img.src = fullUrl;
    }
  }

  // Process all existing images
  function processImages() {
    const images = document.querySelectorAll("img");
    for (const img of images) {
      processImage(img);
    }
  }

  // Run on initial load
  processImages();

  // Watch for dynamically added images
  const observer = new MutationObserver((mutations) => {
    for (const mutation of mutations) {
      if (mutation.type === "childList") {
        for (const node of mutation.addedNodes) {
          if (node.tagName === "IMG") {
            processImage(node);
          } else if (node.querySelectorAll) {
            for (const img of node.querySelectorAll("img")) {
              processImage(img);
            }
          }
        }
      }
    }
  });

  observer.observe(document.body, { childList: true, subtree: true });
})();