Greasy Fork is available in English.

Immich quality switcher

Script for Immich that adds a button to show original image or video instead of scaled down or transcoded version

// ==UserScript==
// @name         Immich quality switcher
// @version      2024-11-30
// @description  Script for Immich that adds a button to show original image or video instead of scaled down or transcoded version
// @author       You
// @match        https://demo.immich.app/*
// @icon         data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @grant        none
// @license      MIT
// @namespace https://greasyfork.org/users/1404702
// ==/UserScript==

// Globals
let IMG_NODE_LIST = [];
let VIDEO_NODE_LIST = [];
let SWITCH_TO_ORIGINAL_BUTTON = null;
let SWITCH_TO_PREVIEW_BUTTON = null;
let SHOW_ORIGINAL = false;

// Main
(function() {
    'use strict';

    console.log("Show original quality image or video Tampermonkey script running");

    window.switchToOriginal = switchToOriginal;
    window.switchToPreview = switchToPreview;

    let selector = "#immich-asset-viewer > div > div > div.flex:has(button)";
    waitForElement(selector, (html_bar) => {
        console.log("Found html bar node " + html_bar);
        addButtonWithSVG(html_bar);
    });
    let imgselector = "#immich-asset-viewer img[src]";
    waitForElement(imgselector, (img_node) => {
        console.log("Found new image node");

        const observer = new MutationObserver((mutations, obs) => {
            console.log("Image list before cleanup");
            console.log(IMG_NODE_LIST);
            IMG_NODE_LIST = IMG_NODE_LIST.filter(entry => {
                if (!entry.node.isConnected) {
                    entry.observer.disconnect();
                    return false
                }
                return true;
            });
            console.log("Image list after cleanup");
            console.log(IMG_NODE_LIST);
            switchToCurrentSelectedQuality();
        });
        observer.observe(img_node, {
            attributes: true,
            childList: true
        });

        IMG_NODE_LIST.push({node: img_node, observer: observer});
        switchToCurrentSelectedQuality();
    });
    let videoselector = "#immich-asset-viewer video[src]";
    waitForElement(videoselector, (video_node) => {
        console.log("Found new video node");

        const observer = new MutationObserver((mutations, obs) => {
            console.log("Video list before cleanup");
            console.log(VIDEO_NODE_LIST);
            VIDEO_NODE_LIST = VIDEO_NODE_LIST.filter(entry => {
                if (!entry.node.isConnected) {
                    entry.observer.disconnect();
                    return false
                }
                return true;
            });
            console.log("Video list after cleanup");
            console.log(VIDEO_NODE_LIST);
            switchToCurrentSelectedQuality();
        });
        observer.observe(video_node, {
            attributes: true,
            childList: true
        });

        VIDEO_NODE_LIST.push({node: video_node, observer: observer});
        switchToCurrentSelectedQuality();
    });
})();


// Helper functions
function switchToCurrentSelectedQuality(selector, callback) {
    console.log("Updating image and video sources to selected quality");
    IMG_NODE_LIST.forEach(entry => {
        let node = entry.node;
        if (SHOW_ORIGINAL && node && node.src && node.src.includes('thumbnail?size=preview&')) {
            node.src = node.src.replace('thumbnail?size=preview&', 'original?');
        }
        if (!SHOW_ORIGINAL && node && node.src && node.src.includes('original?')) {
            node.src = node.src.replace('original?', 'thumbnail?size=preview&');
        }
    });
    VIDEO_NODE_LIST.forEach(entry => {
        let node = entry.node;
        if (SHOW_ORIGINAL && node && node.src && node.src.includes('video/playback')) {
            node.src = node.src.replace('video/playback', 'original');
        }
        if (!SHOW_ORIGINAL && node && node.src && node.src.includes('original')) {
            node.src = node.src.replace('original', 'video/playback');
        }
    });
}

function waitForElement(selector, callback) {
    findNewElementWithCallback(selector, callback);
    const observer = new MutationObserver((mutations, obs) => {
        findNewElementWithCallback(selector, callback);
    });

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

function findNewElementWithCallback(selector, callback) {
    const element = document.querySelectorAll(selector);
    if (element) {
        element.forEach(node => {
            if (!node.hasAttribute('tmElementFound')) {
                node.setAttribute('tmElementFound', '');
                callback(node);
            }
        });
    }
}

function switchToOriginal() {
    console.log("Switching img and video sources to original quality");
    SHOW_ORIGINAL = true;
    switchToCurrentSelectedQuality();
    SWITCH_TO_PREVIEW_BUTTON.removeAttribute('style');
    SWITCH_TO_ORIGINAL_BUTTON.setAttribute('style', 'display:none');
}

function switchToPreview() {
    console.log("Switching img and video sources to preview quality");
    SHOW_ORIGINAL = false;
    switchToCurrentSelectedQuality();
    SWITCH_TO_ORIGINAL_BUTTON.removeAttribute('style');
    SWITCH_TO_PREVIEW_BUTTON.setAttribute('style', 'display:none');
}

function addButtonWithSVG(parentElement) {
    // Create the button element
    var button = document.createElement('button');

    // Set button classes
    button.className = 'flex place-content-center place-items-center rounded-full bg-transparent hover:bg-immich-bg/30 text-white hover:dark:text-white p-3 transition-all disabled:cursor-default hover:dark:text-immich-dark-gray';

    // Set button title attribute
    button.setAttribute('title', 'Show Original');
    button.setAttribute('onclick', 'window.switchToOriginal()');

    if (SHOW_ORIGINAL) {
        // Set second button to hidden
        button.setAttribute('style', 'display:none');
    }

    // Create SVG element
    var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
    svg.setAttribute('width', '24');
    svg.setAttribute('height', '24');
    svg.setAttribute('viewBox', '0 0 24 24');
    svg.setAttribute('fill', 'none');
    svg.setAttribute('stroke', 'currentColor');
    svg.setAttribute('stroke-width', '2');
    svg.setAttribute('stroke-linecap', 'round');
    svg.setAttribute('stroke-linejoin', 'round');

    // Create path element (placeholder path)
    var path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
    path.setAttribute('d', 'M4 4h16v16H4z M8 12h8M12 8v8');

    // Append path to SVG
    svg.appendChild(path);

    // Append SVG to button
    button.appendChild(svg);

    SWITCH_TO_ORIGINAL_BUTTON = button;

    // Insert button as the first child of the parent element
    parentElement.insertBefore(button, parentElement.firstChild);

    ///////////////////
    // Second button that will switch to preview
    ///////////////////

    // Create the button element
    var button2 = document.createElement('button');

    // Set button classes
    button2.className = 'flex place-content-center place-items-center rounded-full bg-transparent hover:bg-immich-bg/30 text-white hover:dark:text-white p-3 transition-all disabled:cursor-default hover:dark:text-immich-dark-gray';

    // Set button title attribute
    button2.setAttribute('title', 'Show Preview');
    button2.setAttribute('onclick', 'window.switchToPreview()');

    if (!SHOW_ORIGINAL) {
        // Set second button to hidden
        button2.setAttribute('style', 'display:none');
    }

    // Create SVG element
    var svg2 = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    svg2.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
    svg2.setAttribute('width', '24');
    svg2.setAttribute('height', '24');
    svg2.setAttribute('viewBox', '0 0 24 24');
    svg2.setAttribute('fill', 'none');
    svg2.setAttribute('stroke', 'currentColor');
    svg2.setAttribute('stroke-width', '2');
    svg2.setAttribute('stroke-linecap', 'round');
    svg2.setAttribute('stroke-linejoin', 'round');

    // Create path element (placeholder path)
    var path2 = document.createElementNS('http://www.w3.org/2000/svg', 'path');
    path2.setAttribute('d', 'M4 4h16v16H4z M8 12h8');

    // Append path to SVG
    svg2.appendChild(path2);

    // Append SVG to button
    button2.appendChild(svg2);

    SWITCH_TO_PREVIEW_BUTTON = button2;

    // Insert button as the first child of the parent element
    parentElement.insertBefore(button2, parentElement.firstChild);
}