// ==UserScript==
// @name Telegram Auto Next & CSS Fullscreen
// @namespace http://tampermonkey.net/
// @version 2.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/a/*
// @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'; // Enforces stricter parsing and error handling in JavaScript.
// --- Log Function ---
/**
* Logs a message to the console with a specific script prefix for easy debugging.
* @param {string} message The message to log.
*/
function log(message) {
// Prepends a unique identifier to all console messages from this script.
console.log(`[TG Media Enhancer] ${message}`);
}
// --- Configuration ---
// The interval (in milliseconds) at which the script checks the state of the media viewer.
const CHECK_INTERVAL_MS = 250;
// The delay (in milliseconds) before automatically switching to the next image.
const IMAGE_SWITCH_DELAY_MS = 2000;
// The time threshold (in seconds) from the end of a video to trigger the switch to the next media.
const VIDEO_END_THRESHOLD_S = 1.0;
// The key used to toggle the script's enabled/disabled state.
const TOGGLE_KEY = 'g';
// A unique ID for the CSS <style> element injected by this script for fullscreen mode.
const FULLSCREEN_STYLE_ID = 'tg-enhancer-fullscreen-style';
// --- State Variables ---
// Holds the current enabled/disabled state of the script.
// The state is persisted across sessions using GM_getValue. Defaults to 'true' (enabled).
let isEnabled = GM_getValue('tgMediaEnhancerEnabled', true);
// Holds the timer ID for the automatic image switch. Used to cancel the timer if needed.
let imageSwitchTimeout = null;
// Stores a reference to the currently displayed media element to prevent reprocessing it.
let processedMediaElement = null;
log(`Initial state loaded. Enabled: ${isEnabled}`);
/**
* Shows a temporary notification overlay on the screen.
* @param {string} message The message to display.
*/
function showNotification(message) {
log(`Showing notification: "${message}"`);
// Find and remove any existing notification to prevent overlap.
const existing = document.getElementById('tg-enhancer-notification');
if (existing) {
log("Removing existing notification.");
existing.remove();
}
// Create the notification element.
const notification = document.createElement('div');
notification.id = 'tg-enhancer-notification';
// Apply CSS styles for positioning, appearance, and transitions.
Object.assign(notification.style, {
position: 'fixed', bottom: '20px', left: '50%', transform: 'translateX(-50%)',
padding: '10px 20px', background: 'rgba(0, 0, 0, 0.75)', color: 'white',
zIndex: '9999', borderRadius: '8px', opacity: '1',
transition: 'opacity 0.5s ease-out', pointerEvents: 'none',
});
notification.textContent = message;
document.body.appendChild(notification);
log("Notification element appended to body.");
// Set a timer to fade out and then remove the notification.
setTimeout(() => {
log("Fading out notification.");
notification.style.opacity = '0';
setTimeout(() => {
log("Removing notification from DOM.");
notification.remove();
}, 500); // Wait for fade-out transition to complete before removing.
}, 2000); // Notification stays visible for 2 seconds.
}
/**
* Removes the fullscreen CSS style from the document head, returning to the default view.
*/
function disableFullscreen() {
log("Attempting to disable fullscreen.");
const styleTag = document.getElementById(FULLSCREEN_STYLE_ID);
if (styleTag) {
styleTag.remove();
log("Fullscreen style tag found and removed.");
} else {
log("Fullscreen style tag not found, no action needed.");
}
}
/**
* Creates or updates the CSS style for a clean, letterboxed/pillarboxed fullscreen media display.
* It calculates the correct scale and translation to fit the media to the screen.
* @param {HTMLElement} contentElement The element containing the media.
*/
function updateOrCreateFullscreenStyle(contentElement, mediaViewer) {
log("Attempting to update or create fullscreen style.");
// Find the elements that define the media's dimensions.
const videoSizer = contentElement.querySelector('.VideoPlayer > div');
const imageElement = contentElement.querySelector('img');
let mediaWidth = 0, mediaHeight = 0;
// Get dimensions from the video sizer element if it exists.
if (videoSizer && videoSizer.style.width) {
mediaWidth = parseFloat(videoSizer.style.width);
mediaHeight = parseFloat(videoSizer.style.height);
log(`Video sizer found. Dimensions: ${mediaWidth}x${mediaHeight}`);
// Otherwise, get dimensions from the image's natural size.
} else if (imageElement) {
mediaWidth = imageElement.naturalWidth;
mediaHeight = imageElement.naturalHeight;
log(`Image element found. Natural dimensions: ${mediaWidth}x${mediaHeight}`);
} else {
log("No video sizer or image element found for style creation.");
}
// Abort if media dimensions are not valid.
if (isNaN(mediaWidth) || mediaWidth === 0) {
log("Media width is invalid or zero, aborting style creation.");
return;
}
// Calculate the scale factor to fit the media within the viewport while maintaining aspect ratio.
const screenWidth = window.innerWidth, screenHeight = window.innerHeight;
const scale = Math.min(screenWidth / mediaWidth, screenHeight / mediaHeight);
// Calculate the translation needed to center the scaled media on the screen.
const translateX = (screenWidth - (mediaWidth * scale)) / 2;
const translateY = (screenHeight - (mediaHeight * scale)) / 2;
// Construct the CSS transform value.
const newTransform = `translate3d(${translateX.toFixed(4)}px, ${translateY.toFixed(4)}px, 0px) scale3d(${scale.toFixed(4)}, ${scale.toFixed(4)}, 1)`;
log(`Calculated fullscreen transform: ${newTransform}`);
// Define the CSS rule to override Telegram's default styles for the active media slide.
const newRule = `
#MediaViewer .MediaViewerSlide--active .MediaViewerContent {
position: fixed !important; top: 0 !important; left: 0 !important;
width: ${mediaWidth}px !important; height: ${mediaHeight}px !important;
transform-origin: 0 0 !important; transform: ${newTransform} !important;
z-index: 1500 !important;
}
`;
// Find the existing style tag or create a new one.
let styleTag = document.getElementById(FULLSCREEN_STYLE_ID);
if (!styleTag) {
log("No existing style tag found, creating a new one.");
styleTag = document.createElement('style');
styleTag.id = FULLSCREEN_STYLE_ID;
document.head.appendChild(styleTag);
log("New style tag appended to head.");
}
// Update the style tag's content only if it has changed.
if (styleTag.textContent !== newRule) {
log("Updating style tag content.");
styleTag.textContent = newRule;
} else {
log("Style rule is already up-to-date.");
}
// For a cleaner view, remove the default header and caption.
const mediaHead = mediaViewer.querySelector('.media-viewer-head');
if (mediaHead) {
mediaHead.remove();
log("Removed media viewer header.");
}
const mediaText = contentElement.querySelector('.media-viewer-footer-content');
if (mediaText) {
mediaText.remove();
log("Removed media text overlay.");
}
}
/**
* Dispatches a keyboard event to the document to simulate pressing the right arrow key,
* which is Telegram's native way to advance to the next media item.
*/
function switchToNext() {
log("Dispatching 'ArrowRight' keydown event to switch to next media.");
const rightArrowEvent = new KeyboardEvent('keydown', {
key: 'ArrowRight', keyCode: 39, which: 39, bubbles: true, cancelable: true
});
document.dispatchEvent(rightArrowEvent);
}
/**
* The main function of the script, called periodically by setInterval.
* It checks the current state of the Telegram media viewer and takes action.
*/
function checkViewState() {
// Check if the main media viewer container is present in the DOM.
const mediaViewer = document.getElementById('MediaViewer');
if (!mediaViewer) {
// If the viewer is closed, perform cleanup.
if (processedMediaElement) {
log("Media viewer is closed. Cleaning up state.");
clearTimeout(imageSwitchTimeout); // Clear any pending image switch.
log("Image switch timeout cleared.");
processedMediaElement = null; // Reset the state.
log("Processed media element reset.");
disableFullscreen(); // Remove the custom fullscreen styles.
}
return; // Exit the function since the viewer isn't open.
}
// If the script is manually disabled, perform cleanup and exit.
if (!isEnabled) {
if (processedMediaElement) {
log("Script is disabled but state exists. Cleaning up.");
clearTimeout(imageSwitchTimeout);
log("Image switch timeout cleared because script is disabled.");
processedMediaElement = null;
log("Processed media element reset because script is disabled.");
disableFullscreen();
}
return;
}
// Find the currently active media slide.
const activeSlide = mediaViewer.querySelector('.MediaViewerSlide--active');
if (!activeSlide) {
// This can happen briefly during transitions, so we just wait for the next check.
return;
}
// Find the container and the actual media element (video or image).
const contentElement = activeSlide.querySelector('.MediaViewerContent');
const currentElement = activeSlide.querySelector('video, img');
if (!contentElement || !currentElement) {
log("No content or media element (video/img) found in the active slide.");
return;
}
// This is the core logic: check if the displayed media is a new one.
if (currentElement !== processedMediaElement) {
log(`New media detected. Old: ${processedMediaElement?.tagName}, New: ${currentElement.tagName}.`);
processedMediaElement = currentElement; // Update state to the new element.
log("Clearing any existing image switch timeout.");
clearTimeout(imageSwitchTimeout); // Cancel previous timer.
updateOrCreateFullscreenStyle(contentElement, mediaViewer); // Apply fullscreen styles.
// If the new media is an image, set a timer to switch to the next one.
if (currentElement.tagName === 'IMG') {
log(`Setting up timer to switch image in ${IMAGE_SWITCH_DELAY_MS}ms.`);
imageSwitchTimeout = setTimeout(() => {
log("Image timer expired. Triggering switch to next media.");
processedMediaElement = null; // Reset state *before* switching to allow the next media to be processed.
switchToNext();
}, IMAGE_SWITCH_DELAY_MS);
}
return; // Return after processing the new media.
}
// If the media is a video that we are already tracking:
if (currentElement.tagName === 'VIDEO') {
const video = currentElement;
// Check if the video is near its end.
const isNearEnd = video.duration && (video.duration - video.currentTime) < VIDEO_END_THRESHOLD_S;
if (isNearEnd) {
log(`Video is near end (currentTime: ${video.currentTime}, duration: ${video.duration}). Triggering switch.`);
processedMediaElement = null; // Reset state before switching.
switchToNext();
}
}
}
// Add a global keyboard listener to handle the toggle key.
document.addEventListener('keydown', (e) => {
log(`Keydown event detected: Key='${e.key}', Target='${e.target.tagName}'.`);
// Ignore key presses if the user is typing in a text field to prevent conflicts.
if (e.target.isContentEditable || e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
log("Keydown event ignored (target is content-editable or an input field).");
return;
}
// Check if the pressed key is the designated toggle key.
if (e.key.toLowerCase() === TOGGLE_KEY) {
log(`Toggle key '${TOGGLE_KEY}' pressed.`);
isEnabled = !isEnabled; // Flip the enabled state.
GM_setValue('tgMediaEnhancerEnabled', isEnabled); // Save the new state.
log(`Script is now ${isEnabled ? 'ENABLED' : 'DISABLED'}. Saved state.`);
showNotification(`Media Enhancer: ${isEnabled ? 'ON' : 'OFF'}`);
if (!isEnabled) {
// If disabled, immediately turn off the fullscreen features.
log("Script disabled, ensuring fullscreen is turned off.");
disableFullscreen();
} else {
// If enabled, reset processed media to force a re-evaluation on the next check.
log("Script enabled, resetting processed media element to force re-evaluation.");
processedMediaElement = null;
}
}
});
// --- Initialization ---
// Start the main loop.
setInterval(checkViewState, CHECK_INTERVAL_MS);
log('Script v2.1 loaded and checkViewState interval started.');
// Show a notification on load to inform the user that the script is active.
showNotification(`Media Enhancer Loaded (Toggle key: G)`);
})();