Immich quality switcher

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

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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);
}