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.

As of 2025-08-08. See the latest version.

// ==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。