NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==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 });
})();