Fullres Zoomable Twitch Thumbnails

Modifies twitch thumbnails to be full resolution images instead of downscaled where possible and allows zoom on hover.

// ==UserScript==
// @name         Fullres Zoomable Twitch Thumbnails
// @namespace    http://tampermonkey.net/
// @version      2024-12-05
// @description  Modifies twitch thumbnails to be full resolution images instead of downscaled where possible and allows zoom on hover.
// @author       laund
// @license      MIT
// @match        https://www.twitch.tv/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=twitch.tv
// @grant        none
// @require      https://cdn.jsdelivr.net/gh/CoeJoder/waitForKeyElements.js@a03933c5e42343b434c7800eb2777575342d8287/waitForKeyElements.js
// @require      https://gist.githubusercontent.com/laundmo/341526bd819c619712e2680a4469cd7f/raw/19621be01b830c0f888c99769e2964f40a5282bf/hover_zoomable.js#sha256=166177289f4c2707a86f057b9861d29daea4ed1ef9e6b33aaf5fb39cf331d991
// ==/UserScript==
/* globals waitForKeyElements, makeImageHoverZoomable */

const twitch_cdn_url_size_regex = /(thumb0)?(?:-\d{1,5}x\d{1,5})(\.jpg|\.png|\.jpeg|)/gm;

const selectors = [
    ".preview-card-image-link .tw-image:not(.thumbnail_fullres_modified)",
    ".search-result-card__img-wrapper .tw-image.search-result-card__img:not(.thumbnail_fullres_modified)",
    ".search-result-related-live-channels__row-container .tw-image:not(.thumbnail_fullres_modified)"
];

waitForKeyElements(selectors.join(", "), handleNewThumbnail, false, 200);
waitForKeyElements(".preview-card-thumbnail__image:not(.preview-card-thumbnail__image--animated) .tw-image:not(.thumbnail_fullres_modified)", handleNewThumbnailCloned, false, 200);

function replacer(match, g1, g2){
    if (g1 === undefined){
        return g2
    }
    return g1 + "-1920x1080" + g2
}


function handleNewThumbnail(elem){
    elem.classList.add(".thumbnail_fullres_modified");
    elem.src = elem.src.replace(twitch_cdn_url_size_regex, replacer);
    makeImageHoverZoomable(elem, 3, 0.5);
}


function handleNewThumbnailCloned(elem){
    // sometimes the selector matches again after cloning
    if (elem.classList.contains(".thumbnail_fullres_modified")) { return; }

    // clone container
    const container = elem.parentNode;
    // make sure its not copying src from a animated thumbnail
    if (container.classList.contains(".preview-card-thumbnail__image--animated")){ return; }
    const cloned = container.cloneNode(true);
    container.hidden = true;

    // handle normally
    handleNewThumbnail(cloned.querySelector(".tw-image"));

    // reinstate container
    container.parentNode.insertBefore(cloned, container.nextSibling);
}