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