Telegram Auto Next & CSS Fullscreen

Automatically enters CSS web fullscreen on video or image load and clicks 'next' on video end or image showed after 2s. Toggle with 'G' key.

Fra 08.08.2025. Se den seneste versjonen.

// ==UserScript==
// @name         Telegram Auto Next & CSS Fullscreen
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  Automatically enters CSS web fullscreen on video or image load and clicks 'next' on video end or image showed after 2s. Toggle with 'G' key.
// @author       CurssedCoffin (perfected with gemini) https://github.com/CurssedCoffin
// @match        https://web.telegram.org/k/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=telegram.org
// @grant        GM_setValue
// @grant        GM_getValue
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    /**
     * @description Standardized logging function for easy debugging.
     * @param {string} message - The message to be printed to the console.
     */
    function log(message) {
        console.log(`[TG Auto Next & CSS Fullscreen] ${message}`);
    }

    // ========================================================================
    // --- Core Feature 1: The VSR Root-Cause Fix (Lean Version) ---
    // Description: Telegram's chat background canvas interferes with the graphics driver's
    //      hardware acceleration detection for the main video stream. This function
    //      removes the canvas upon page load, fundamentally fixing the issue where
    //      VSR/RSR wouldn't activate in windowed mode.
    // ========================================================================
    
    /**
     * @description Precisely finds and removes the conflicting background canvas element.
     */
    function fixVsr() {
        // Use a precise and efficient DOM query with ID and class name.
        const backgroundCanvas = document.querySelector('#page-chats .chat-background-item-pattern-canvas');
        
        // If the element is found, remove it from the DOM.
        if (backgroundCanvas) {
            backgroundCanvas.remove();
            log('VSR Fix: Found and removed the conflicting background canvas.');
        }
    }
    
    // Execute the fix once, 500ms after the script starts, to ensure a stable DOM.
    setTimeout(fixVsr, 500);
    log('VSR Fix: VSR fix initialized, will run for the first time in 500ms.');


    // ========================================================================
    // --- Core Feature 2: Integrated Logic for Smart Fullscreen & Autoplay ---
    // Description: Uses a single toggle ('G' key) to control the "Auto Fullscreen & Next" mode.
    //      In this mode, all images and videos will automatically enter a CSS-based fullscreen,
    //      and the script will proceed to the next item when conditions are met.
    // ========================================================================
    
    // --- Configuration Constants ---
    const STORAGE_KEY = 'telegramAutoplayState';                    // Key for storing the user's toggle state in GM storage.
    const CHECK_INTERVAL = 250;                                     // The main loop's check interval in milliseconds for faster response.
    const IMAGE_VIEW_DELAY = 2000;                                  // Duration to display an image before proceeding, in milliseconds.
    const VIDEO_END_THRESHOLD_SECONDS = 1.0;                        // Threshold in seconds to consider a video "near the end".
    const FULLSCREEN_STYLE_ID = 'tg-js-fullscreen-style';           // The ID for our dynamically created <style> tag for easy management.

    // --- State Variables ---
    // Reads the last toggle state from storage, defaulting to false (off) on first run.
    let isCombinedModeEnabled = GM_getValue(STORAGE_KEY, false);
    let imageTimerId = null;                                        // Stores the setTimeout ID for the image timer.
    let processedMediaElement = null;                               // Tracks the currently processed media element to prevent redundant actions.

    /**
     * @description Disables the CSS fullscreen by removing our dynamically injected <style> tag.
     */
    function disableJsFullscreen() {
        const existingStyle = document.getElementById(FULLSCREEN_STYLE_ID);
        if (existingStyle) {
            existingStyle.remove();
            log('JS Fullscreen: Style tag removed, exiting fullscreen mode.');
        }
    }

    /**
     * @description Creates or updates the fullscreen style. This function implements the efficient
     *              "create once, update many" logic.
     * @param {HTMLElement} moverElement - The media container element, i.e., .media-viewer-mover.
     */
    function updateOrCreateJsFullscreen(moverElement) {
        // Directly read the pristine width and height from the container's style attribute.
        // This is the most direct and reliable method.
        const mediaWidth = parseFloat(moverElement.style.width);
        const mediaHeight = parseFloat(moverElement.style.height);

        // If valid dimensions cannot be obtained, return to prevent calculation errors.
        if (isNaN(mediaWidth) || isNaN(mediaHeight) || mediaWidth === 0 || mediaHeight === 0) {
            log('JS Fullscreen Warning: Could not get media dimensions, skipping fullscreen.');
            return;
        }

        // Get the viewport dimensions.
        const screenWidth = window.innerWidth;
        const screenHeight = window.innerHeight;

        // Calculate the scale factor, taking the smaller of the width/height ratios
        // to ensure the entire media fits within the screen (a "contain" effect).
        const scale = Math.min(screenWidth / mediaWidth, screenHeight / mediaHeight);

        // Calculate the translation needed on X and Y axes to center the scaled media.
        const translateX = (screenWidth - (mediaWidth * scale)) / 2;
        const translateY = (screenHeight - (mediaHeight * scale)) / 2;

        // Construct the final CSS transform value.
        const newTransform = `translate3d(${translateX}px, ${translateY}px, 0px) scale3d(${scale}, ${scale}, 1)`;

        // Construct the CSS rule text to be injected. Uses !important to ensure it overrides Telegram's native styles.
        const newRule = `
            .media-viewer-mover.active.center {
                left: 0 !important; top: 0 !important;
                transform: ${newTransform} !important;
            }
        `;

        // Attempt to get the <style> tag; create it if it doesn't exist.
        let styleTag = document.getElementById(FULLSCREEN_STYLE_ID);
        if (!styleTag) {
            styleTag = document.createElement('style');
            styleTag.id = FULLSCREEN_STYLE_ID;
            document.head.appendChild(styleTag);
            log('JS Fullscreen: First run, creating style tag.');
        }
        
        // Only update the content if the new rule is different, preventing unnecessary DOM reflows.
        if (styleTag.textContent !== newRule) {
            styleTag.textContent = newRule;
            log(`JS Fullscreen: Style updated, scale: ${scale.toFixed(2)}`);
        }
    }

    /**
     * @description The main loop function, called periodically by setInterval, drives the script's core functionality.
     */
    function checkAndPlayNext() {
        // If the master toggle is off, ensure fullscreen is disabled and return immediately.
        if (!isCombinedModeEnabled) {
            disableJsFullscreen();
            return;
        }

        // Find the main media viewer elements.
        const activeViewer = document.querySelector('.media-viewer-whole.active');
        
        // If the media viewer is not open, clean up all states and return.
        if (!activeViewer) {
            if (processedMediaElement) {
                log('Media viewer closed, cleaning up state...');
                clearTimeout(imageTimerId);
                processedMediaElement = null;
                disableJsFullscreen();
            }
            return;
        }

        // Find necessary control and media elements.
        const nextButton = activeViewer.querySelector('span.tgico.media-viewer-sibling-button.media-viewer-next-button');
        const moverElement = activeViewer.querySelector('.media-viewer-mover.active.center');
        const videoElement = activeViewer.querySelector('video.ckin__video');
        const imageOrCanvasElement = activeViewer.querySelector('.media-viewer-aspecter > img'); // You changed this to only find img
        const currentElement = videoElement || imageOrCanvasElement;

        // If any critical element is missing, reset state and wait for the next check.
        if (!nextButton || !moverElement || !currentElement) {
            processedMediaElement = null;
            return;
        }

        // --- Logic Split Point 1: Processing New Media ---
        // If the current media is different from the last processed one, it's new content.
        if (currentElement !== processedMediaElement) {
            log(`New media detected: ${currentElement.tagName}`);
            processedMediaElement = currentElement;
            clearTimeout(imageTimerId); // Clear any previous image timer.
            
            // Apply fullscreen style for the new media.
            updateOrCreateJsFullscreen(moverElement);

            // Remove any potential caption.
            const captionElement = activeViewer.querySelector('.media-viewer-caption');
            if (captionElement) {
                captionElement.remove();
                log('Media caption removed.');
            }

            // Remove any potential topbar.
            const topbarElement = activeViewer.querySelector('.media-viewer-topbar');
            if (topbarElement) {
                topbarElement.remove();
                log('Media topbar removed.');
            }

            // If it's an image, set a timer to click "next" after the specified delay.
            if (imageOrCanvasElement) {
                log(`Image loaded, will switch after ${IMAGE_VIEW_DELAY}ms.`);
                imageTimerId = setTimeout(() => {
                    log('Image timer expired, clicking "next".');
                    nextButton.click();
                }, IMAGE_VIEW_DELAY);
            }
            
            // [CRITICAL] Return immediately after initializing new media to prevent
            // incorrectly checking video state in the same cycle.
            return;
        }

        // --- Logic Split Point 2: Checking State of Loaded Media ---
        // This part only runs if the media is not "new".
        if (videoElement) {
            // Robustness check: ensure the video's duration is loaded and valid to prevent "quick skipping".
            if (isNaN(videoElement.duration) || videoElement.duration === 0) return;
            
            // Check if the video is near its end.
            const isNearEnd = videoElement.currentTime >= videoElement.duration - VIDEO_END_THRESHOLD_SECONDS;
            
            // If the video has ended or is near the end, reset state and click "next".
            if (videoElement.ended || isNearEnd) {
                log(`Video near end (current: ${videoElement.currentTime.toFixed(2)}s, duration: ${videoElement.duration.toFixed(2)}s), clicking "next".`);
                processedMediaElement = null;
                nextButton.click();
            }
        }
    }

    // ========================================================================
    // --- Core Feature 3: Event Listeners ---
    // ========================================================================
    document.addEventListener('keydown', function(e) {
        // Do not respond to hotkeys if the user is typing in an input field.
        if (e.target.isContentEditable || e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
        
        // The 'G' key is the universal toggle for the "Auto Fullscreen & Next" mode.
        if (e.key.toLowerCase() === 'g') {
            isCombinedModeEnabled = !isCombinedModeEnabled;
            // Persist the new state to GM storage for the next session.
            GM_setValue(STORAGE_KEY, isCombinedModeEnabled);
            const statusText = `Auto Fullscreen & Next has been ${isCombinedModeEnabled ? 'Enabled' : 'Disabled'}`;
            log(statusText);
            showNotification(statusText);

            // If disabling the mode, perform cleanup immediately for instant feedback.
            if (!isCombinedModeEnabled) {
                clearTimeout(imageTimerId);
                processedMediaElement = null;
                disableJsFullscreen();
            }
        }
    });

    // --- Startup and Notifications ---
    /**
     * @description Displays a short-lived status notification in the bottom center of the page.
     * @param {string} message - The message text to display.
     */
    function showNotification(message) {
        // Remove any existing notification to prevent overlap.
        const existingNotification = document.querySelector('.tg-autoplay-notification');
        if (existingNotification) existingNotification.remove();
        
        const notification = document.createElement('div');
        notification.style.cssText = `
            position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
            background-color: rgba(0, 0, 0, 0.75); color: white; padding: 10px 20px;
            border-radius: 8px; z-index: 9999; font-size: 16px;
            transition: opacity 0.5s ease-in-out; opacity: 1;
        `;
        notification.className = 'tg-autoplay-notification';
        notification.textContent = message;
        document.body.appendChild(notification);
        
        // Automatically fade out and remove the notification after 3 seconds.
        setTimeout(() => {
            notification.style.opacity = '0';
            setTimeout(() => notification.remove(), 500);
        }, 3000);
    }

    // Start the main loop.
    setInterval(checkAndPlayNext, CHECK_INTERVAL);
    
    // Display the initial status notification when the script is loaded.
    const initialStateMessage = `TG Script loaded (VSR fixed). Press 'G' to toggle Auto Fullscreen & Next.`;
    showNotification(initialStateMessage);
    log('Script initialization complete.');

})();
长期地址
遇到问题?请前往 GitHub 提 Issues。