• úvod
  • témata
  • události
  • tržiště
  • diskuze
  • nástěnka
  • přihlásit
    registrace
    ztracené heslo?
    TOMTampermonkey 🐒 - máte užitečný skript? a mohli bychom ho vidět?
    https://www.tampermonkey.net/
    kód samotného skriptu vkládejte takto (viz také nápověda vpravo nahoře):
    ```js
    text_skriptu
    ```
    Browsery (kromě Yandexu) podporující extensions včetně tampermonkey:
    Edge Beta
    https://play.google.com/store/apps/details?id=com.microsoft.emmx.beta
    FDroid
    Fennec F-Droid | F-Droid - Free and Open Source Android App Repository
    https://f-droid.org/packages/org.mozilla.fennec_fdroid/
    rozbalit záhlaví
    PICAZSOO
    PICAZSOO --- ---
    A možná prosím opravte info v záhlaví, že jediný android browser, co umí user scripty je Edge Beta? Když Firefox for Android (teda přinejmenším Fennec z F-Droidu) podporuje jak Tampermonkey, tak Violentmonkey.

    Za mě je to nejdospělejší prohlížeč na telefon. Možná pomalejší než Chrome nebo Edge, ale na Galaxy S24 rozdíl v praxi nevidím.
    PICAZSOO
    PICAZSOO --- ---
    Velmi dlouho jsem na svém android telefonu používal Operu Mobile. Důvod byl, že jako jediný prohlížeč podporovala text reflow při změně zoomu ( typicky situace, kdy nějaká mobile-unfriendly stránka má moc široký odstavce a malý text a člověk je nucenej po zoomu scrolovat doprava a doleva. Např. některý klasický diskuzní fóra.) Tohle opera vždycky řešila úplně perfektně.

    Na PC ale používám Firefox a už mě nebavilo, jak to nefunguje dohromady z hlediska nějakýho sdílení záložek a historie atd. Takže jsem začal používat Fennec [1], který má slušnou podporu rozšíření a používal jsem tam rozšíření Text reflow on zoom (text wrap) [2]. Fungovalo to skoro stejně dobře jako ta funkcionalita v Opeře, ale mělo to pár much - některý textový prvky to newrapuje, vůbec to nesahá na dynamicky načtený nový prvky (typicky načtení dalších komentářů v diskuzi na redditu atd.) a nepodporovalo to double-tap-to-zoom (jenom pinch-to-zoom).

    Naštěstí je ten plugin open source a MIT licensed a funguje i jako user script. Tak jsem s pomocí Gemini odstranil mouchy, co mě trápily a teď ho používám skrz Violentmonkey [3] a jsem extrémně spokojenej.

    [1] https://f-droid.org/packages/org.mozilla.fennec_fdroid/
    [2] https://addons.mozilla.org/en-US/android/addon/text-reflow-on-zoom-mobile/
    [3] https://addons.mozilla.org/cs/firefox/addon/violentmonkey/

    Skript zde (permanentně zapnutý):
    // ==UserScript==
    // @name         Text reflow on zoom for mobile (text wrap) - Mixed Content Fix
    // @name:ru      Text reflow on zoom for mobile (text wrap) - Mixed Content Fix
    // @description  Fits all text to the screen width after a pinch or double-tap zoom gesture (fixes mixed text/quote blocks)
    // @description:ru Подгонка текста под ширину экрана после жеста увеличения на телефоне
    // @version      1.0.12
    // @author       emvaized
    // @license      MIT
    // @homepageURL  https://github.com/emvaized/text-reflow-on-zoom-mobile/
    // @namespace    text_reflow_on_pinch_zoom
    // @match        *://*/*
    // @grant        none
    // @run-at       document-start
    // ==/UserScript==
    
    (function() {
        'use strict';
    
        // UPDATED SELECTOR: Uses text()[normalize-space()] to find ANY valid text node, not just the first one.
        const xpathSelector = `
        //p |
        //li |
        //h1 | //h2 | //h3 | //h4 | //h5 | //h6 |
        //pre |
        //a[text()[normalize-space()]] |
        //div[b or em or i] |
        //div[text()[normalize-space()]] |
        //div[span[text()[normalize-space()]]]`;
    
        let isCssInjected = false;
        let isPinching = false;
        let zoomTarget, targetDyOffsetRatio;
        let resizeDebounceTimer;
        let mutationDebounceTimer;
        
        // Track viewport width to ignore height-only changes (keyboard)
        let lastViewportWidth = window.visualViewport ? window.visualViewport.width : window.innerWidth;
    
        // Track all text elements queried by the selector
        const allTextElements = new Set();
    
        /**
         * Reflows text to fit screen.
         * @param {boolean} shouldRestoreScroll - If true, scrolls back to the zoom target. False for passive updates (infinite scroll).
         */
        function reflowText(shouldRestoreScroll = false) {
            if (!isCssInjected) {
                const styleContent = `.text-reflow-userscript { word-wrap: break-word !important; overflow-wrap:break-word !important; max-width:var(--text-reflow-max-width) !important; }
                .text-reflow-scroll-padding {scroll-margin-left: 1vw !important;}`;
                const styleElement = document.createElement('style');
                styleElement.textContent = styleContent;
                document.head.appendChild(styleElement);
                isCssInjected = true;
            }
    
            // Calculate new width based on the visual viewport (zoomed area)
            const maxAllowedWidth = Math.round(window.visualViewport.width * 0.96);
            document.documentElement.style.setProperty('--text-reflow-max-width', `${maxAllowedWidth}px`);
    
            // Update our width tracker
            lastViewportWidth = window.visualViewport.width;
    
            // Select elements likely to contain text
            const xpathResult = document.evaluate(xpathSelector, document, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
    
            // Refresh text element set
            allTextElements.clear();
    
            for (let i = 0, n = xpathResult.snapshotLength, el; i < n; i++) {
                el = xpathResult.snapshotItem(i);
                if (!el.offsetParent) continue;
                // Double check text content to be safe
                if (!el.textContent.trim()) continue;
    
                // Process only top-level text elements
                let isTopLevel = true;
                let parent = el.parentElement;
                while (parent) {
                    if (elementIsTextElement(parent)) {
                        isTopLevel = false;
                        break;
                    }
                    parent = parent.parentElement;
                }
    
                if (isTopLevel) {
                    el.classList.add('text-reflow-userscript');
                    allTextElements.add(el);
                }
             }
    
            /// Scroll initial target element into view (ONLY if this is a zoom-triggered reflow)
            if (shouldRestoreScroll && zoomTarget && targetDyOffsetRatio) {
                try {
                    // Scroll to element vertically, according to new page layout
                    const targetOffset = targetDyOffsetRatio * window.innerHeight;
                    const rect = zoomTarget.getBoundingClientRect();
                    const targetTop = rect.top + window.pageYOffset;
                    const scrollToPosition = targetTop - targetOffset;
    
                    window.scrollTo({
                        top: scrollToPosition,
                        behavior: 'instant'
                    });
    
                    // Scroll element into view horizontally if it's text
                    if (zoomTarget.nodeName !== 'IMG' && zoomTarget.nodeName !== 'VIDEO' && zoomTarget.nodeName !== 'IFRAME'){
                        zoomTarget.classList.add('text-reflow-scroll-padding');
                        zoomTarget.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'start' });
                        zoomTarget.classList.remove('text-reflow-scroll-padding');
                    }
                } catch (e) {
                    // Fail silently if target is detached from DOM
                }
                
                zoomTarget = null;
                targetDyOffsetRatio = null;
            }
        }
    
        function elementIsTextElement(element) {
            return allTextElements.has(element);
        }
    
        // --- Interaction Handlers ---
    
        function handleTouchStart(event) {
            if (!event.touches) return;
    
            // Logic for tracking the target element
            let midpointX, midpointY;
    
            if (event.touches.length >= 2) {
                isPinching = true;
                const touch1 = event.touches[0];
                const touch2 = event.touches[1];
                midpointX = (touch1.clientX + touch2.clientX) / 2;
                midpointY = (touch1.clientY + touch2.clientY) / 2;
            } else {
                midpointX = event.touches[0].clientX;
                midpointY = event.touches[0].clientY;
            }
    
            let possibleZoomTarget;
            const elementsFromPoint = document.elementsFromPoint(midpointX, midpointY);
    
            for (let i = 0, n = elementsFromPoint.length, element; i < n; i++) {
                element = elementsFromPoint[i];
                if (elementIsTextElement(element)) {
                    possibleZoomTarget = element;
                    break;
                }
            }
    
            if (!possibleZoomTarget) possibleZoomTarget = elementsFromPoint[0];
    
            if (possibleZoomTarget) {
                zoomTarget = possibleZoomTarget;
                const targetRect = zoomTarget.getBoundingClientRect();
                targetDyOffsetRatio = targetRect.top / window.innerHeight;
            }
        }
    
        function handleTouchEnd(event) {
            if (isPinching && (event.touches && event.touches.length === 0)) {
                isPinching = false;
                reflowText(true);
            }
        }
    
        function handleViewportResize() {
            if (isPinching) return;
            
            const currentWidth = window.visualViewport.width;
            if (Math.abs(currentWidth - lastViewportWidth) < 1) {
                return;
            }
    
            clearTimeout(resizeDebounceTimer);
            resizeDebounceTimer = setTimeout(() => {
                reflowText(true);
            }, 400); 
        }
    
        // --- Dynamic Content Handling ---
        function initObserver() {
            if (!document.body) return;
    
            const observer = new MutationObserver((mutations) => {
                if (isPinching) return;
    
                let hasAddedNodes = false;
                for (const mutation of mutations) {
                    if (mutation.addedNodes.length > 0) {
                        hasAddedNodes = true;
                        break;
                    }
                }
    
                if (hasAddedNodes) {
                    clearTimeout(mutationDebounceTimer);
                    mutationDebounceTimer = setTimeout(() => {
                        reflowText(false);
                    }, 500);
                }
            });
    
            observer.observe(document.body, { childList: true, subtree: true });
        }
    
        // --- Listeners ---
        window.addEventListener('touchstart', handleTouchStart, { passive: true });
        window.addEventListener('touchend', handleTouchEnd);
        
        if (window.visualViewport) {
            window.visualViewport.addEventListener('resize', handleViewportResize);
        }
    
        if (document.body) {
            initObserver();
        } else {
            window.addEventListener('DOMContentLoaded', initObserver);
        }
    
    })();


    a tady je ještě novější verze, u které lze podržením tří prstů na displeji text-wrap globálně vypnout a zapnout podle potřeby (když třeba zlobí na některých stránkách).

    // ==UserScript==
    // @name         Text reflow on zoom for mobile (text wrap) - Sync Fix
    // @name:ru      Text reflow on zoom for mobile (text wrap) - Sync Fix
    // @description  Fits all text to the screen width after a pinch or double-tap zoom gesture (Global 3-finger toggle)
    // @description:ru Подгонка текста под ширину экрана после жеста увеличения на телефоне (Глобальный Вкл/Выкл 3 пальцами)
    // @version      1.0.20
    // @author       emvaized
    // @license      MIT
    // @homepageURL  https://github.com/emvaized/text-reflow-on-zoom-mobile/
    // @namespace    text_reflow_on_pinch_zoom
    // @match        *://*/*
    // @grant        GM_setValue
    // @grant        GM_getValue
    // @grant        GM_addValueChangeListener
    // @run-at       document-start
    // ==/UserScript==
    
    (function() {
        'use strict';
    
        const ENABLED_CLASS = 'userscript-mobile-text-reflow-enabled';
    
        // v1.0.12 Selector
        const xpathSelector = `
        //p |
        //a[normalize-space(text())] |
        //h1 | //h2 | //h3 | //h4 | //h5 | //h6 |
        //li |
        //pre |
        //div[b or em or i] |
        //div[text()[normalize-space()]] |
        //div[span[text()[normalize-space()]]]`;
    
        let isReflowEnabled = GM_getValue('reflowEnabled', true);
        
        let isCssInjected = false;
        let isPinching = false;
        
        let zoomTarget, targetDyOffsetRatio;
        let resizeDebounceTimer;
        let mutationDebounceTimer;
        
        let longPressTimer;
        const LONG_PRESS_DURATION = 800; 
        const MOVE_THRESHOLD = 15;
        let startTouches = [];
        
        let lastViewportWidth = window.visualViewport ? window.visualViewport.width : window.innerWidth;
    
        const allTextElements = new Set();
        let notificationEl = null;
    
        // Apply initial state
        updateHtmlClass();
    
        // 1. Listen for background changes (if browser allows)
        if (typeof GM_addValueChangeListener !== 'undefined') {
            GM_addValueChangeListener('reflowEnabled', (name, oldVal, newVal, remote) => {
                if (isReflowEnabled !== newVal) {
                    isReflowEnabled = newVal;
                    updateHtmlClass();
                    if (isReflowEnabled) reflowText(false); 
                }
            });
        }
    
        // 2. FORCE SYNC on tab switch (Fixes frozen background tabs)
        document.addEventListener("visibilitychange", () => {
            if (document.visibilityState === 'visible') {
                const globalState = GM_getValue('reflowEnabled', true);
                if (isReflowEnabled !== globalState) {
                    isReflowEnabled = globalState;
                    updateHtmlClass();
                    if (isReflowEnabled) reflowText(false);
                }
            }
        });
    
        function updateHtmlClass() {
            if (isReflowEnabled) {
                document.documentElement.classList.add(ENABLED_CLASS);
            } else {
                document.documentElement.classList.remove(ENABLED_CLASS);
                document.documentElement.style.setProperty('--text-reflow-max-width', 'unset');
            }
        }
    
        function showNotification(text) {
            if (!notificationEl) {
                notificationEl = document.createElement('div');
                Object.assign(notificationEl.style, {
                    position: 'fixed',
                    top: '50%',
                    left: '50%',
                    backgroundColor: 'rgba(0, 0, 0, 0.9)',
                    color: 'white',
                    padding: '16px 24px',
                    borderRadius: '12px',
                    zIndex: '2147483647',
                    fontFamily: 'sans-serif',
                    fontSize: '18px',
                    fontWeight: 'bold',
                    pointerEvents: 'none',
                    opacity: '0',
                    transition: 'opacity 0.2s ease',
                    textAlign: 'center',
                    boxShadow: '0 4px 15px rgba(0,0,0,0.5)',
                    userSelect: 'none' 
                });
                document.body.appendChild(notificationEl);
            }
    
            const scale = window.visualViewport ? (1 / window.visualViewport.scale) : 1;
            notificationEl.style.transform = `translate(-50%, -50%) scale(${scale})`;
            
            notificationEl.textContent = text;
            notificationEl.style.opacity = '1';
    
            setTimeout(() => {
                notificationEl.style.opacity = '0';
            }, 2000);
        }
    
        function reflowText(shouldRestoreScroll = false) {
            if (!isReflowEnabled) return;
    
            if (!isCssInjected) {
                const styleContent = `html.${ENABLED_CLASS} .text-reflow-userscript { word-wrap: break-word !important; overflow-wrap:break-word !important; max-width:var(--text-reflow-max-width) !important; }
                .text-reflow-scroll-padding {scroll-margin-left: 1vw !important;}`;
                const styleElement = document.createElement('style');
                styleElement.textContent = styleContent;
                document.head.appendChild(styleElement);
                isCssInjected = true;
            }
    
            const maxAllowedWidth = Math.round(window.visualViewport.width * 0.96);
            document.documentElement.style.setProperty('--text-reflow-max-width', `${maxAllowedWidth}px`);
    
            lastViewportWidth = window.visualViewport.width;
    
            const xpathResult = document.evaluate(xpathSelector, document, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
    
            allTextElements.clear();
    
            for (let i = 0, n = xpathResult.snapshotLength, el; i < n; i++) {
                el = xpathResult.snapshotItem(i);
                if (!el.offsetParent) continue;
                if (!el.textContent.trim()) continue;
    
                let isTopLevel = true;
                let parent = el.parentElement;
                while (parent) {
                    if (elementIsTextElement(parent)) {
                        isTopLevel = false;
                        break;
                    }
                    parent = parent.parentElement;
                }
    
                if (isTopLevel) {
                    el.classList.add('text-reflow-userscript');
                    allTextElements.add(el);
                }
             }
    
            if (shouldRestoreScroll && zoomTarget && targetDyOffsetRatio) {
                try {
                    const targetOffset = targetDyOffsetRatio * window.innerHeight;
                    const rect = zoomTarget.getBoundingClientRect();
                    const targetTop = rect.top + window.pageYOffset;
                    const scrollToPosition = targetTop - targetOffset;
    
                    window.scrollTo({
                        top: scrollToPosition,
                        behavior: 'instant'
                    });
    
                    if (zoomTarget.nodeName !== 'IMG' && zoomTarget.nodeName !== 'VIDEO' && zoomTarget.nodeName !== 'IFRAME'){
                        zoomTarget.classList.add('text-reflow-scroll-padding');
                        zoomTarget.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'start' });
                        zoomTarget.classList.remove('text-reflow-scroll-padding');
                    }
                } catch (e) {
                    // Fail silently
                }
                zoomTarget = null;
                targetDyOffsetRatio = null;
            }
        }
    
        function elementIsTextElement(element) {
            return allTextElements.has(element);
        }
    
        // --- Interaction Handlers ---
    
        function handleTouchStart(event) {
            if (!event.touches) return;
    
            // Toggle Gesture (3 Fingers)
            if (event.touches.length === 3) {
                startTouches = [
                    {x: event.touches[0].clientX, y: event.touches[0].clientY},
                    {x: event.touches[1].clientX, y: event.touches[1].clientY},
                    {x: event.touches[2].clientX, y: event.touches[2].clientY}
                ];
    
                clearTimeout(longPressTimer);
                longPressTimer = setTimeout(() => {
                    isReflowEnabled = !isReflowEnabled;
                    GM_setValue('reflowEnabled', isReflowEnabled);
                    updateHtmlClass();
                    showNotification(isReflowEnabled ? "Reflow: ON" : "Reflow: OFF");
                    if (isReflowEnabled) reflowText(true);
                    longPressTimer = null;
                }, LONG_PRESS_DURATION);
                return; 
            }
    
            let midpointX, midpointY;
    
            if (event.touches.length >= 2) {
                isPinching = true;
                const touch1 = event.touches[0];
                const touch2 = event.touches[1];
                midpointX = (touch1.clientX + touch2.clientX) / 2;
                midpointY = (touch1.clientY + touch2.clientY) / 2;
            } else {
                midpointX = event.touches[0].clientX;
                midpointY = event.touches[0].clientY;
            }
    
            if (isReflowEnabled) {
                let possibleZoomTarget;
                const elementsFromPoint = document.elementsFromPoint(midpointX, midpointY);
    
                for (let i = 0, n = elementsFromPoint.length, element; i < n; i++) {
                    element = elementsFromPoint[i];
                    if (elementIsTextElement(element)) {
                        possibleZoomTarget = element;
                        break;
                    }
                }
    
                if (!possibleZoomTarget) possibleZoomTarget = elementsFromPoint[0];
    
                if (possibleZoomTarget) {
                    zoomTarget = possibleZoomTarget;
                    const targetRect = zoomTarget.getBoundingClientRect();
                    targetDyOffsetRatio = targetRect.top / window.innerHeight;
                }
            }
        }
    
        function handleTouchMove(event) {
            if (longPressTimer && event.touches.length === 3 && startTouches.length === 3) {
                for (let i = 0; i < 3; i++) {
                    const dx = event.touches[i].clientX - startTouches[i].x;
                    const dy = event.touches[i].clientY - startTouches[i].y;
                    if (Math.abs(dx) > MOVE_THRESHOLD || Math.abs(dy) > MOVE_THRESHOLD) {
                        clearTimeout(longPressTimer);
                        longPressTimer = null;
                        return;
                    }
                }
                return;
            }
            
            if (longPressTimer && event.touches.length !== 3) {
                 clearTimeout(longPressTimer);
                 longPressTimer = null;
            }
        }
    
        function handleTouchEnd(event) {
            if (longPressTimer) {
                 clearTimeout(longPressTimer);
                 longPressTimer = null;
            }
    
            if (isPinching && (event.touches && event.touches.length === 0)) {
                isPinching = false;
                if (isReflowEnabled) reflowText(true);
            }
        }
    
        function handleViewportResize() {
            if (isPinching || !isReflowEnabled) return;
            
            const currentWidth = window.visualViewport.width;
            if (Math.abs(currentWidth - lastViewportWidth) < 1) {
                return;
            }
    
            clearTimeout(resizeDebounceTimer);
            resizeDebounceTimer = setTimeout(() => {
                reflowText(true);
            }, 400); 
        }
    
        function initObserver() {
            if (!document.body) return;
    
            const observer = new MutationObserver((mutations) => {
                if (isPinching || !isReflowEnabled) return;
    
                let hasAddedNodes = false;
                for (const mutation of mutations) {
                    if (mutation.addedNodes.length > 0) {
                        hasAddedNodes = true;
                        break;
                    }
                }
    
                if (hasAddedNodes) {
                    clearTimeout(mutationDebounceTimer);
                    mutationDebounceTimer = setTimeout(() => {
                        reflowText(false);
                    }, 500);
                }
            });
    
            observer.observe(document.body, { childList: true, subtree: true });
        }
    
        window.addEventListener('touchstart', handleTouchStart, { passive: true });
        window.addEventListener('touchmove', handleTouchMove, { passive: true });
        window.addEventListener('touchend', handleTouchEnd);
        
        if (window.visualViewport) {
            window.visualViewport.addEventListener('resize', handleViewportResize);
        }
    
        if (document.body) {
            initObserver();
        } else {
            window.addEventListener('DOMContentLoaded', initObserver);
        }
    
    })();

    Třeba se to bude někomu hodit. Obecně jsem u těhle user scriptů s tím kóděním přes gemini a chatGpt udělal dobrou zkušenost. Programovat umím, ale na tyhle věci mi nezbývá čas.
    MUXX
    MUXX --- ---
    Na reddit jsem nedavno objevil bajecnou vec - https://gosinkit.com/
    Neni to tampermonkey, ale stoji to za to.
    STARF
    STARF --- ---
    pokud pouzivate Reddit, posty s vice obrazkama je standardne nutny proklikat mysi... tenhle script prida carousel scroll pomoci sipek na klavesnici
    prevzato z: https://github.com/TheFantasticLoki/Tampermonkey-Scripts/blob/master/Reddit%20Image%20Gallery%20Arrow%20Navigation.user.js

    // ==UserScript==
    // @name         Reddit Image Gallery Arrow Navigation
    // @namespace    https://reddit.com/
    // @version      1.9.2
    // @description  Navigate Reddit image galleries using arrow keys, made with the help of ChatGPT, Thanks ChatGPT!
    // @author       TheFantasticLoki
    // @match        https://*.reddit.com/*
    // @grant        none
    // @license      MIT
    // @homepage     https://github.com/TheFantasticLoki/Tampermonkey-Scripts
    // @homepageURL  https://github.com/TheFantasticLoki/Tampermonkey-Scripts
    // @supportURL   https://github.com/TheFantasticLoki/Tampermonkey-Scripts/issues
    // @downloadURL  https://github.com/TheFantasticLoki/Tampermonkey-Scripts/raw/refs/heads/master/Reddit%20Image%20Gallery%20Arrow%20Navigation.user.js
    // @updateURL    https://github.com/TheFantasticLoki/Tampermonkey-Scripts/raw/refs/heads/master/Reddit%20Image%20Gallery%20Arrow%20Navigation.user.js
    // ==/UserScript==
    
    (function () {
        'use strict';
    
        // Set the debug variable to control logging
        const debug = false;
    
        let activeGallery = null;
    
        const simulateClick = (button) => {
            if (button) {
                try {
                    const event = new MouseEvent('click', {
                        bubbles: true,
                        cancelable: true,
                        view: window,
                    });
                    button.dispatchEvent(event);
                    if (debug) console.log('Dispatched click event successfully');
                } catch (err) {
                    if (debug) console.error('Error dispatching click event:', err);
                }
            } else {
                if (debug) console.log('Button not interactable or not found.');
            }
        };
    
        const handleKeyDown = (e) => {
            if (document.activeElement.tagName === 'INPUT' || document.activeElement.tagName === 'TEXTAREA') {
                return;
            }
    
            if (!activeGallery) {
                if (debug) console.log('No active gallery to navigate.');
                return;
            }
    
            if (e.key === 'ArrowLeft') {
                if (debug) console.log('Left arrow pressed');
                const prevButton = activeGallery.querySelector('button[aria-label="Previous page"]');
                if (debug) console.log('Previous Button:', prevButton);
                simulateClick(prevButton);
            }
    
            if (e.key === 'ArrowRight') {
                if (debug) console.log('Right arrow pressed');
                const nextButton = activeGallery.querySelector('button[aria-label="Next page"]');
                if (debug) console.log('Next Button:', nextButton);
                simulateClick(nextButton);
            }
        };
    
        const handleMouseEnter = (e) => {
            const galleryCarousel = e.currentTarget;
            if (galleryCarousel && galleryCarousel.shadowRoot) {
                activeGallery = galleryCarousel.shadowRoot;
                if (debug) console.log('Mouse entered gallery-carousel. Active gallery set:', activeGallery);
            }
        };
    
        const handleMouseLeave = () => {
            activeGallery = null;
            if (debug) console.log('Mouse left gallery-carousel. Active gallery cleared.');
        };
    
        const detectPopup = () => {
            const lightbox = document.querySelector('#shreddit-media-lightbox');
            if (lightbox) {
                const gallery = lightbox.querySelector('gallery-carousel');
                if (gallery && gallery.shadowRoot) {
                    activeGallery = gallery.shadowRoot;
                    if (debug) console.log('Gallery popup detected. Active gallery set:', activeGallery);
                }
            }
        };
    
        const observeDOMChanges = () => {
            const observer = new MutationObserver((mutations) => {
                mutations.forEach((mutation) => {
                    if (mutation.addedNodes) {
                        mutation.addedNodes.forEach((node) => {
                            if (node.id === 'shreddit-media-lightbox') {
                                if (debug) console.log('Lightbox detected:', node);
                                detectPopup();
                            }
                        });
                    }
                });
            });
    
            observer.observe(document.body, { childList: true, subtree: true });
        };
    
        const attachHoverListeners = () => {
            document.querySelectorAll('gallery-carousel').forEach((carousel) => {
                carousel.addEventListener('mouseenter', handleMouseEnter);
                carousel.addEventListener('mouseleave', handleMouseLeave);
            });
        };
    
        const observeFeed = () => {
            const feedObserver = new MutationObserver(() => {
                attachHoverListeners(); // Reattach listeners as new galleries load
            });
    
            feedObserver.observe(document.body, { childList: true, subtree: true });
        };
    
        // Initial setup
        document.addEventListener('keydown', handleKeyDown);
        observeDOMChanges();
        observeFeed();
    
        // Attach listeners for initial galleries
        attachHoverListeners();
    
        if (debug) console.log('Script initialized.');
    })();
    TOM
    TOM --- ---
    TOM: zapomněl jsem dodat, že nemá GUI - ovládá se jenom klávesovou zkratkou Ctrl+Shift+mezerník
    TOM
    TOM --- ---
    pauzne/pustí video na všech tabech/stránkách prohlížeče, kde je skript aktivní
    asterisk (*://*/*) případně změňte na youtube apod.
    hodí se např. pokud potřebujete dabing z nekvalitního videa a video obraz z jiného (kombinace youtube + netflix)

    // ==UserScript==
    // @name Pause/Play an all tabs
    // @namespace http://tampermonkey.net/
    // @version 1.0
    // @description Pause/play all videos across all tabs with Ctrl+Shift+Space
    // @match *://*/*
    // @grant GM_setValue
    // @grant GM_getValue
    // @grant GM_addValueChangeListener
    // @run-at document-idle
    // ==/UserScript==
    
    (function() {
    'use strict';
    
    // ============ CONFIGURATION ============
    const CONFIG = {
    debug: true, // Console logging
    storageKey: 'globalVideoToggle'
    };
    
    const log = (...args) => CONFIG.debug && console.log('[VideoToggle]', ...args);
    
    // ============ VIDEO CONTROL ============
    function toggleVideos(action) {
    const videos = document.querySelectorAll('video');
    let affected = 0;
    
    videos.forEach(video => {
    try {
    if (action === 'pause') {
    if (!video.paused) {
    video.pause();
    affected++;
    }
    } else {
    if (video.paused) {
    video.play().catch(() => {}); // Ignore autoplay restrictions
    affected++;
    }
    }
    } catch (e) {
    log('Error controlling video:', e);
    }
    });
    
    log(`${action.toUpperCase()}: ${affected}/${videos.length} videos`);
    }
    
    // ============ KEYBOARD SHORTCUT ============
    document.addEventListener('keydown', (e) => {
    // Ctrl+Shift+Space
    if (e.ctrlKey && e.shiftKey && e.code === 'Space') {
    e.preventDefault();
    e.stopPropagation();
    
    // Toggle based on previous state
    const current = GM_getValue(CONFIG.storageKey, { action: 'play' });
    const newAction = current.action === 'pause' ? 'play' : 'pause';
    
    log(`Shortcut triggered -> ${newAction}`);
    
    // Broadcast to all tabs (including this one via the listener)
    GM_setValue(CONFIG.storageKey, {
    action: newAction,
    timestamp: Date.now() // Ensures value always changes
    });
    
    // Apply locally (listener may not fire for local changes in some versions)
    toggleVideos(newAction);
    }
    }, true); // Capture phase - catches event before websites can intercept
    
    // ============ CROSS-TAB LISTENER ============
    GM_addValueChangeListener(CONFIG.storageKey, (name, oldValue, newValue, remote) => {
    if (remote) { // Only react to changes from OTHER tabs
    log(`Remote signal received -> ${newValue.action}`);
    toggleVideos(newValue.action);
    }
    });
    
    // ============ INIT ============
    log('Loaded. Press Ctrl+Shift+Space to pause/play all videos across tabs.');
    })();
    TOM
    TOM --- ---
    brute force scraper titulků ze streamu
    běží minimálně na stránce arte.tv (konkrétní tip zde), jinde jsem to nepotřeboval/netestoval
    skript nemá GUI, přehrávané titulky se stáhnou po skončení videa automaticky do nastavené dwl složky prohlížeče

    // ==UserScript==
    // @name         Arte.tv Auto Subtitle Scraper
    // @namespace    http://tampermonkey.net/
    // @version      3.0
    // @description  Automatically scrape and save subtitles on tab close/navigation
    // @match        https://www.arte.tv/*
    // @match        https://*.arte.tv/*
    // @grant        none
    // @run-at       document-idle
    // ==/UserScript==
    (function() {
        'use strict';
        // ============ CONFIGURATION ============
        const CONFIG = {
            // Console logging
            debug: true,
            // Start scraping automatically
            autoStart: true,
            // Auto-save when leaving page
            saveOnExit: true,
            // Auto-save on URL/navigation change
            saveOnUrlChange: true,
            // Minimum subtitles before auto-saving
            minSubtitles: 10,
            // Manual save shortcut
            keyboardShortcut: 'ctrl+shift+s',
            // Filename prefix
            filenamePrefix: 'arte_subtitles_',
        };
        // Note: Download folder is controlled by browser settings, not this script.
        // Files will save to your browser's default download location.
        // ============ STATE ============
        const log = (...args) => CONFIG.debug && console.log('[SubScraper]', ...args);
        let capturedSubs = [];
        let isScanning = false;
        let hasAutoSaved = false;
        // ============ FILTERS ============
        const UI_GARBAGE = [
            'Pause', 'Play', 'Mute', 'Unmute',
            'Forward 10 seconds', 'Back 10 seconds',
            'Rewind 10 seconds', 'Skip 10 seconds',
            'Settings', 'Fullscreen', 'Exit fullscreen',
            'Subtitles', 'Audio', 'Quality', 'Captions',
            'Volume', 'Speed', 'Next', 'Previous',
            'Share', 'Download', 'More', 'Less'
        ];
        function isUIGarbage(text) {
            if (!text || text.length < 2) return true;
            // Percentages like "1.36%"
            if (/^\d+\.?\d*%$/.test(text)) return true;
            // Timestamps like "12:34" or "1:23:45"
            if (/^\d{1,2}:\d{2}(:\d{2})?$/.test(text)) return true;
            // Known UI text
            if (UI_GARBAGE.some(ui => text.includes(ui))) return true;
            // Very short or pure numbers
            if (text.length < 3 || /^\d+$/.test(text)) return true;
            return false;
        }
        // ============ SCRAPING ============
        function startScraping() {
            if (isScanning) {
                log('Already scanning');
                return;
            }
            isScanning = true;
            log('Auto-scraping started');
            const observer = new MutationObserver((mutations) => {
                if (!isScanning) return;
                mutations.forEach(mutation => {
                    mutation.addedNodes.forEach(node => {
                        let text = '';
                        if (node.nodeType === Node.TEXT_NODE) {
                            text = node.textContent.trim();
                        } else if (node.nodeType === Node.ELEMENT_NODE) {
                            text = node.textContent.trim();
                        }
                        if (text && !isUIGarbage(text)) {
                            captureSubtitle(text);
                        }
                    });
                });
            });
            // Monitor subtitle areas
            const targets = document.querySelectorAll('.avp, video, [class*="player"], [class*="subtitle"], [class*="caption"]');
            targets.forEach(target => {
                observer.observe(target, {
                    childList: true,
                    subtree: true,
                    characterData: true
                });
            });
            log(`Monitoring ${targets.length} elements`);
        }
        function captureSubtitle(text) {
            // Avoid duplicates
            if (capturedSubs.some(s => s.text === text)) return;
            const video = document.querySelector('video');
            const timestamp = video ? video.currentTime : capturedSubs.length * 2;
            capturedSubs.push({
                start: timestamp,
                end: timestamp + 2, // Default 2 second duration
                text: text
            });
            log(`[${capturedSubs.length}] ${text.substring(0, 50)}`);
        }
        // ============ OVERLAP CORRECTION ============
        function fixOverlaps(subs) {
            // Sort by start time
            subs.sort((a, b) => a.start - b.start);
            // Fix overlaps: if current subtitle ends after next starts,
            // adjust current end to match next start
            for (let i = 0; i < subs.length - 1; i++) {
                if (subs[i].end > subs[i + 1].start) {
                    log(`Overlap fixed: [${i}] ${formatTime(subs[i].end)} -> ${formatTime(subs[i + 1].start)}`);
                    subs[i].end = subs[i + 1].start;
                }
            }
            return subs;
        }
        // ============ DEDUPLICATION ============
        function removeDuplicates(subs) {
            const unique = [];
            const seen = new Set();
            subs.forEach(sub => {
                const key = `${sub.start.toFixed(1)}_${sub.text}`;
                if (!seen.has(key)) {
                    seen.add(key);
                    unique.push(sub);
                }
            });
            return unique;
        }
        // ============ SRT GENERATION ============
        function generateSRT() {
            if (capturedSubs.length === 0) {
                log('No subtitles to save');
                return null;
            }
            // Process subtitles
            let processed = removeDuplicates(capturedSubs);
            processed = fixOverlaps(processed);
            log(`Generating SRT: ${capturedSubs.length} raw -> ${processed.length} final`);
            // Convert to SRT format
            let srt = '';
            processed.forEach((sub, idx) => {
                srt += `${idx + 1}\n`;
                srt += `${formatTime(sub.start)} --> ${formatTime(sub.end)}\n`;
                srt += `${sub.text}\n\n`;
            });
            return srt;
        }
        function formatTime(seconds) {
            const h = Math.floor(seconds / 3600);
            const m = Math.floor((seconds % 3600) / 60);
            const s = Math.floor(seconds % 60);
            const ms = Math.floor((seconds % 1) * 1000);
            return `${pad(h)}:${pad(m)}:${pad(s)},${pad(ms, 3)}`;
        }
        function pad(num, size = 2) {
            let s = num + '';
            while (s.length < size) s = '0' + s;
            return s;
        }
        // ============ SAVE ============
        function saveSubtitles(reason = 'manual') {
            if (capturedSubs.length < CONFIG.minSubtitles) {
                log(`Not saving: only ${capturedSubs.length} subtitles (min: ${CONFIG.minSubtitles})`);
                return false;
            }
            const srt = generateSRT();
            if (!srt) return false;
            const filename = `${CONFIG.filenamePrefix}${Date.now()}.srt`;
            try {
                const blob = new Blob([srt], { type: 'text/plain;charset=utf-8' });
                const url = URL.createObjectURL(blob);
                const a = document.createElement('a');
                a.href = url;
                a.download = filename;
                // For auto-save on exit, we need to click synchronously
                document.body.appendChild(a);
                a.click();
                document.body.removeChild(a);
                // Clean up after a delay
                setTimeout(() => URL.revokeObjectURL(url), 100);
                log(`✓ Saved: ${filename} (${capturedSubs.length} lines, reason: ${reason})`);
                return true;
            } catch (e) {
                console.error('[SubScraper] Save failed:', e);
                return false;
            }
        }
        // ============ AUTO-SAVE TRIGGERS ============
        // Tab close / page refresh / browser close
        window.addEventListener('beforeunload', (e) => {
            if (CONFIG.saveOnExit && !hasAutoSaved) {
                hasAutoSaved = true;
                saveSubtitles('page-exit');
            }
        });
        // URL change (SPA navigation)
        if (CONFIG.saveOnUrlChange) {
            let currentUrl = window.location.href;
            setInterval(() => {
                if (window.location.href !== currentUrl) {
                    log('URL changed detected');
                    currentUrl = window.location.href;
                    if (!hasAutoSaved) {
                        hasAutoSaved = true;
                        saveSubtitles('url-change');
                    }
                }
            }, 1000);
        }
        // ============ KEYBOARD SHORTCUT ============
        document.addEventListener('keydown', (e) => {
            // Ctrl+Shift+S (or Cmd+Shift+S on Mac)
            if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key.toLowerCase() === 's') {
                e.preventDefault();
                log('Manual save triggered');
                saveSubtitles('keyboard');
            }
        });
        // ============ INITIALIZATION ============
        function init() {
            if (!document.body) {
                setTimeout(init, 100);
                return;
            }
            log('Arte.tv Auto Subtitle Scraper v3.0');
            log(`Config: autoStart=${CONFIG.autoStart}, saveOnExit=${CONFIG.saveOnExit}`);
            log(`Keyboard shortcut: ${CONFIG.keyboardShortcut.toUpperCase()} for manual save`);
            if (CONFIG.autoStart) {
                // Wait for video player to load
                setTimeout(() => {
                    const video = document.querySelector('video');
                    if (video) {
                        log('Video detected, starting scraper');
                        startScraping();
                    } else {
                        log('No video found, waiting...');
                        setTimeout(init, 2000);
                    }
                }, 2000);
            }
        }
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', init);
        } else {
            init();
        }
        // ============ DEBUG INFO ============
        // Expose stats for console inspection
        window.subScraperStats = () => {
            return {
                captured: capturedSubs.length,
                autoSaved: hasAutoSaved,
                config: CONFIG
            };
        };
        log('Tip: Run window.subScraperStats() in console to see current status');
    })();
    TOM
    TOM --- ---
    POSEIDON: ¯\_(°-°)_/¯ napsal jsem podle aktuální situace, nevím jestli/kdy/proč se může gcp zobrazit
    tvoje verze bude tím pádem určitě spolehlivější
    POSEIDON
    POSEIDON --- ---
    TOM: /gcp/ uz neni potreba?
    // ==UserScript==
    // @name         AliExpress Redirect Script
    // @namespace    http://tampermonkey.net/
    // @version      1.0
    // @description  Redirect from AliExpress /gcp/ URLs to /item/ URLs using productIds param
    // @author       Poseidon
    // @match        https://www.aliexpress.com/*
    // @grant        none
    // ==/UserScript==
    
    (function() {
        'use strict';
    
        // Get current URL
        const url = new URL(window.location.href);
    
        if (url.pathname.includes("/gcp/") || url.pathname.includes("/ssr/")) {
            if (url.searchParams.has("productIds")){
                // Get the value of productIds param
                const productIds = url.searchParams.get("productIds");
                // Split by colon and take the first ID
                const productId = productIds.split(':')[0];
    
                // Create the new URL
                const newUrl = `https://www.aliexpress.com/item/${productId}.html`;
    
                // Redirect to the new URL
                window.location.replace(newUrl);
            }
        }
    })();
    TOM
    TOM --- ---
    Pokud vás obtěžují bundle deals na Aliexpressu, tak tohle je vypne (respektive vezme první ProductID z toho bundle, a přepíše URL adresu)
    // ==UserScript==
    // @name         AliExpress Bundle to Single Item
    // @namespace    http://tampermonkey.net/
    // @version      1.0
    // @description  Redirect bundle deals to first item
    // @match        https://www.aliexpress.com/*
    // @match        https://aliexpress.com/*
    // @grant        none
    // @run-at       document-start
    // ==/UserScript==
    
    (function() {
        'use strict';
        const url = window.location.href;
        // Check for bundle deal URL
        if (url.includes('aliexpress.com/ssr/')) {
            const urlParams = new URLSearchParams(window.location.search);
            const productIds = urlParams.get('productIds');
            if (productIds) {
                // Extract first product ID (digits before colon/separator)
                const match = productIds.match(/^(\d+)/);
                if (match) {
                    window.location.replace(`https://www.aliexpress.com/item/${match[1]}.html`);
                }
            }
        }
    })();
    SHASHA_KRASTY
    SHASHA_KRASTY --- ---
    pro ty co pouzivají vymeny nesmyslu z inventare ve sluzbe Steam, tak zde jeden, skoro bych rekl, nepostradatelny:
    https://www.steamtradematcher.com/res/userscript/stm.user.js

    TOM
    TOM --- ---
    jinak pro mazání objektů na stránkách používám tohle
    Click To Remove Element - blade.sk
    https://blade.sk/projects/ctre/
    primitivním klikáním na věci, co nechcete nikdy (zaškrtávátkem "Remember by default") nechcete vidět (bloky s reklamou apod.)
    ZORBEN
    ZORBEN --- ---
    XARGH: až budu mít čas, dám tomu šanci
    XARGH
    XARGH --- ---
    ZORBEN: me nakonec pomohl ten redirector, co mi TOM doporucil. nastav to dle SS a budes to mit taky
    .

    ZORBEN
    ZORBEN --- ---
    XARGH: brave (v angličtině) používám jako defaultni prohlížeč, tampermonkey jsem díky tomuhle auditku vyzkoušel poprvý, ale v brave mi to právě nefungovalo... takže jsem byl rad za tvůj první příspěvek a doufal jsem že se dozvím proč to nejde... xcancel jsem znal, jen to přepisovat ručně nechci, víceméně na X koukám jen na odkazy ze 3. světový... zkoušel jsem i nějaký easy debugy abych v konzoli viděl jestli se skript vůbec spustil nebo ne a po pár minutách jsem to vzdal s tím, že brave sám dost věcí blokuje, tak tohle prostě nemá šanci :)))
    ale když se to tady podaří vyřešit, budu rád
    TOM
    TOM --- ---
    XARGH: chápu
    ideální by bylo zkusit jiný skript - chceš něco vytvořit přímo na míru?
    anebo pro tuhle konkrétní potřebu zkus jiný addon ;)
    Redirector - Chrome Web Store
    https://chromewebstore.google.com/detail/jegbdohdgebjljoljfeinojeobdabpjo
    TOM
    TOM --- ---
    XARGH: vytvořil jsem si teď kvůli tomu znovu účet, a oproti xcancel nevidím rozdíl
    komentáře jsou replies ¯\_(°_°)_/¯
    XARGH
    XARGH --- ---
    TOM: odinstalovano. skusil jsem to same v brave a take nic. prah IT frustrace prekrocen :-/

    π (final scene)
    https://www.youtube.com/watch?v=1U1PM-p3860
    XARGH
    XARGH --- ---
    TOM: na rucne prepsanem xcancel nevidim komentare, ale vidim Replies
    TOM
    TOM --- ---
    XARGH: co jiný prohlížeč? MSEdge třeba?
    Kliknutím sem můžete změnit nastavení reklam