Перемотка видео стрелками и восстановление по автору и времени
// ==UserScript==
// @name Telegram Web — перемотка + сохранение прогресса видео
// @version 1.1
// @description Перемотка видео стрелками и восстановление по автору и времени
// @match https://web.telegram.org/*
// @grant none
// @namespace https://greasyfork.org/users/789838
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// --- Configuration ---
const STORAGE_KEY = 'tg_video_progress';
const REWIND_TIME_SECONDS = 5; // Time to jump forward/backward on arrow key press
const SAVE_PROGRESS_INTERVAL_MS = 2000; // How often to save video progress
// --- Utility Functions ---
/**
* Loads video progress from localStorage.
* @returns {Object} An object mapping video keys to their last known playback time.
*/
const loadProgress = () => {
try {
const data = localStorage.getItem(STORAGE_KEY);
return data ? JSON.parse(data) : {};
} catch (e) {
console.error('Telegram Video Progress: Error loading progress from localStorage:', e);
return {};
}
};
/**
* Saves video progress to localStorage.
* @param {Object} obj The object to save.
*/
const saveProgress = obj => {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(obj));
} catch (e) {
console.error('Telegram Video Progress: Error saving progress to localStorage:', e);
}
};
/**
* Checks if an element is visibly rendered on the page.
* @param {HTMLElement} el The element to check.
* @returns {boolean} True if the element is visible, false otherwise.
*/
const isVisible = el => {
return el && el.offsetParent !== null &&
el.offsetWidth > 0 &&
el.offsetHeight > 0 &&
window.getComputedStyle(el).visibility !== 'hidden' &&
window.getComputedStyle(el).display !== 'none'; // Added display check
};
// --- Keyboard Event Handler for Rewind ---
// Using a Set to keep track of active interval IDs for each video
const activeIntervals = new Map();
document.addEventListener('keydown', e => {
// Check if the focus is on an input field to prevent unintended rewinds
if (document.activeElement && (document.activeElement.tagName === 'INPUT' || document.activeElement.tagName === 'TEXTAREA')) {
return;
}
if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
// Find the currently active/visible video.
// Prioritize videos within the media viewer if open, otherwise any visible video.
let video = document.querySelector('.media-viewer-content video');
if (!video || !isVisible(video)) {
const videos = Array.from(document.querySelectorAll('video')).filter(v => isVisible(v));
video = videos[0]; // Take the first visible video if media viewer is not active
}
if (video && !video.paused && !video.ended) { // Only rewind if video is playing
const newTime = video.currentTime + (e.key === 'ArrowRight' ? REWIND_TIME_SECONDS : -REWIND_TIME_SECONDS);
video.currentTime = Math.max(0, Math.min(newTime, video.duration || newTime)); // Clamp to valid range
e.stopPropagation(); // Stop event propagation to prevent Telegram's default actions
e.preventDefault(); // Prevent default browser action (e.g., scrolling)
}
}
}, true); // Use `true` for capture phase to ensure it runs before Telegram's handlers
// --- Media Viewer Observer ---
const observer = new MutationObserver(mutations => {
// Optimized to only re-evaluate when necessary
const mediaViewer = document.querySelector('.media-viewer-modal, .media-viewer-backdrop'); // More robust selector for the viewer
if (!mediaViewer || !isVisible(mediaViewer)) {
// If media viewer is closed or not visible, clear all intervals
activeIntervals.forEach(intervalId => clearInterval(intervalId));
activeIntervals.clear();
return;
}
const nameEl = mediaViewer.querySelector('.media-viewer-name .peer-title, .media-viewer-modal .ChannelInfo-title, .media-viewer-modal .PrivateChatInfo-title'); // Broader selection for titles
const dateEl = mediaViewer.querySelector('.media-viewer-date, .media-viewer-modal .ChatInfo-date'); // Broader selection for dates
const video = mediaViewer.querySelector('video');
if (!nameEl || !dateEl || !video || !isVisible(video)) {
return;
}
const name = nameEl.textContent.trim();
const date = dateEl.textContent.trim();
// Use a combination of URL and date to make the key more unique for the same author on different dates
const videoSrc = video.src || video.currentSrc;
const key = `${name} @ ${date} ${videoSrc}`;
let store = loadProgress();
// Restore progress
// Use a unique dataset attribute or a WeakMap for tracking to avoid conflicts
if (store[key] && !video.dataset.tgProgressRestored) {
video.currentTime = store[key];
video.dataset.tgProgressRestored = 'true'; // Mark as restored
console.log(`Telegram Video Progress: Restored progress for "${name}" from ${store[key].toFixed(2)}s`);
}
// Set up interval for saving progress
// Ensure only one interval per video instance
if (!activeIntervals.has(key)) {
// Clear any existing interval for this key if it somehow lingered
if (activeIntervals.has(key)) {
clearInterval(activeIntervals.get(key));
}
const intervalId = setInterval(() => {
if (video.paused || video.ended || !isVisible(video)) {
// Clear interval if video is paused, ended, or no longer visible
clearInterval(intervalId);
activeIntervals.delete(key);
console.log(`Telegram Video Progress: Stopped saving progress for "${name}".`);
return;
}
// Only save if progress changed meaningfully to avoid excessive writes
if (Math.abs(store[key] - video.currentTime) > 1) { // Save if changed by more than 1 second
store[key] = video.currentTime;
saveProgress(store);
// console.log(`Telegram Video Progress: Saved progress for "${name}" to ${video.currentTime.toFixed(2)}s`);
}
}, SAVE_PROGRESS_INTERVAL_MS);
activeIntervals.set(key, intervalId);
console.log(`Telegram Video Progress: Started saving progress for "${name}".`);
}
});
// Observe the document body for changes, particularly when the media viewer opens/closes
observer.observe(document.body, { childList: true, subtree: true });
console.log('Telegram Video Progress: Script initialized.');
})();