您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Toggles between elapsed time and remaining time with a simple click on the timestamp.
// ==UserScript== // @name YouTube Time Toggle // @namespace YTTimeToggle // @version 1.0.1 // @description Toggles between elapsed time and remaining time with a simple click on the timestamp. // @author Farhan Sakib Socrates // @match *://www.youtube.com/* // @grant none // @license MIT // ==/UserScript== (function() { 'use strict'; // State variable to track whether to show remaining time or elapsed time let isShowingRemainingTime = false; let isDraggingSeekbar = false; // State variable to track seekbar dragging // DOM element references let timeDisplaySpan = null; // Our custom span to display time let videoElement = null; // The YouTube video element let timeDisplayContainer = null; // The main clickable container (.ytp-time-display.notranslate) let timeContentsContainer = null; // The specific parent for current/duration time (.ytp-time-contents) let timeCurrentElement = null; // YouTube's native current time element (.ytp-time-current) let timeSeparatorElement = null; // YouTube's native time separator element (.ytp-time-separator) let timeDurationElement = null; // YouTube's native duration element (.ytp-time-duration) let progressBar = null; // The YouTube progress bar element (.ytp-progress-bar) /** * Formats a given number of seconds into M:SS or H:MM:SS format. * @param {number} totalSeconds - The total number of seconds to format. * @returns {string} The formatted time string. */ function formatTime(totalSeconds) { // Ensure seconds are non-negative totalSeconds = Math.max(0, totalSeconds); const hours = Math.floor(totalSeconds / 3600); const minutes = Math.floor((totalSeconds % 3600) / 60); const remainingSeconds = Math.floor(totalSeconds % 60); let formatted = ''; if (hours > 0) { formatted += hours + ':'; formatted += (minutes < 10 ? '0' : '') + minutes + ':'; } else { formatted += minutes + ':'; } formatted += (remainingSeconds < 10 ? '0' : '') + remainingSeconds; return formatted; } /** * Updates the text content of the custom time display span. * It calculates the time to display based on the current mode (elapsed or remaining). * @param {number} [manualTime] - Optional: A specific time in seconds to display, * used during seekbar dragging. If not provided, * videoElement.currentTime is used. */ function updateTimeDisplay(manualTime = null) { if (!videoElement || !timeDisplaySpan) { // If essential elements are not available, show a placeholder if the span exists if (timeDisplaySpan) { timeDisplaySpan.textContent = '--:-- / --:--'; } return; } // Use manualTime if provided (during drag), otherwise use actual video currentTime let currentTime = (manualTime !== null) ? manualTime : videoElement.currentTime; let duration = videoElement.duration; // Defensive checks for NaN values to prevent "NaN / NaN" display if (isNaN(currentTime)) { currentTime = 0; } // If duration is NaN or 0, it means video metadata might not be fully loaded or it's a live stream without a known end. // In such cases, we can't calculate remaining time reliably. if (isNaN(duration) || duration === 0) { // If duration is unknown, display elapsed time only or a placeholder for total timeDisplaySpan.textContent = formatTime(currentTime) + ' / --:--'; return; // Exit early as remaining time calculation is not possible } let displayTime; let prefix = ''; if (isShowingRemainingTime) { displayTime = duration - currentTime; prefix = '-'; // Add a minus sign for remaining time } else { displayTime = currentTime; } // Update the text content of our custom span with the formatted time and include the separator timeDisplaySpan.textContent = prefix + formatTime(displayTime) + ' / '; } /** * Handles mousemove event during seekbar dragging to update time in real-time. * This function will be attached to the document when dragging starts. */ function handleSeekbarMouseMoveDuringDrag() { if (isDraggingSeekbar && progressBar) { // Read the aria-valuenow attribute for the current scrub position const scrubTime = parseFloat(progressBar.getAttribute('aria-valuenow')); if (!isNaN(scrubTime)) { updateTimeDisplay(scrubTime); // Pass the scrub time to update the display } } } /** * Initializes the userscript by finding necessary DOM elements, injecting the overlay, * setting up event listeners, and performing the initial time display update. * @returns {boolean} True if initialization was successful, false otherwise. */ function initializePlayer() { // Find the main video player and time display elements based on the current YouTube structure videoElement = document.querySelector('video'); timeDisplayContainer = document.querySelector('.ytp-time-display.notranslate'); // The main clickable area timeContentsContainer = document.querySelector('.ytp-time-display.notranslate .ytp-time-contents'); timeCurrentElement = document.querySelector('.ytp-time-display.notranslate .ytp-time-current'); timeSeparatorElement = document.querySelector('.ytp-time-display.notranslate .ytp-time-separator'); timeDurationElement = document.querySelector('.ytp-time-display.notranslate .ytp-time-duration'); progressBar = document.querySelector('.ytp-progress-bar'); // Get the progress bar element // If any essential element is not found, return false to indicate that the player is not ready if (!videoElement || !timeDisplayContainer || !timeContentsContainer || !timeCurrentElement || !timeSeparatorElement || !timeDurationElement || !progressBar) { // console.log('YouTube Elapsed/Remaining Time Toggle: Essential elements not found yet.'); return false; } // Check if our custom span already exists to prevent re-initialization if (timeContentsContainer.querySelector('.yt-custom-time-display')) { // console.log('YouTube Elapsed/Remaining Time Toggle: Already initialized.'); return true; // Already initialized } // 1. Hide YouTube’s native current-time element and separator // Setting display to none ensures they don't take up space in the flex container. timeCurrentElement.style.display = 'none'; timeSeparatorElement.style.display = 'none'; // 2. Inject our custom <span> into .ytp-time-contents timeDisplaySpan = document.createElement('span'); timeDisplaySpan.className = 'yt-custom-time-display'; // Custom class for easy identification // Inherit styling from the parent for seamless integration timeDisplaySpan.style.color = 'inherit'; timeDisplaySpan.style.fontFamily = 'inherit'; timeDisplaySpan.style.fontSize = 'inherit'; timeDisplaySpan.style.fontWeight = 'inherit'; timeDisplaySpan.style.lineHeight = '1'; // Ensure single line height timeDisplaySpan.style.whiteSpace = 'nowrap'; // Prevent wrapping of the time string // Insert our custom span directly before the duration element within the contents container // This makes it flow naturally with the duration element. timeContentsContainer.insertBefore(timeDisplaySpan, timeDurationElement); // 3. Update the overlay: // Normal playback updates videoElement.addEventListener('timeupdate', () => { // Only update via timeupdate if not currently dragging if (!isDraggingSeekbar) { updateTimeDisplay(); } }); // The 'seeked' event is crucial for updating after a seek operation is complete. // Introduce a small delay to ensure videoElement.currentTime is stable. videoElement.addEventListener('seeked', () => { setTimeout(() => { updateTimeDisplay(); // Update using videoElement.currentTime after a short delay }, 50); // 50ms delay }); videoElement.addEventListener('durationchange', updateTimeDisplay); // Real-time update while dragging the seekbar progressBar.addEventListener('mousedown', () => { isDraggingSeekbar = true; // Attach mousemove listener to the document to capture movement anywhere on the page during drag document.addEventListener('mousemove', handleSeekbarMouseMoveDuringDrag); }); // Use document for mouseup to catch releases even if mouse leaves the progress bar document.addEventListener('mouseup', () => { if (isDraggingSeekbar) { isDraggingSeekbar = false; // Remove the document-level mousemove listener document.removeEventListener('mousemove', handleSeekbarMouseMoveDuringDrag); // The 'seeked' event listener (with its delay) will handle the final update. } }); // 5. Click on the time area toggles the display mode // Attach the click listener to the larger timeDisplayContainer. timeDisplayContainer.style.cursor = 'pointer'; // Indicate the larger area is clickable timeDisplayContainer.addEventListener('click', (event) => { // Prevent the click from bubbling up to other elements that might have listeners event.stopPropagation(); isShowingRemainingTime = !isShowingRemainingTime; updateTimeDisplay(); }); // Initial update updateTimeDisplay(); console.log('YouTube Elapsed/Remaining Time Toggle: Initialized successfully.'); return true; } // Main MutationObserver to detect player readiness // This observer watches for the presence of the main player controls container. const mainObserver = new MutationObserver((mutations, obs) => { // Check for a key element that indicates the player controls are loaded if (document.querySelector('.ytp-chrome-bottom')) { if (initializePlayer()) { obs.disconnect(); // Player initialized, no need to observe anymore for initial load } } }); // Start observing the document body for changes in its children and descendants. mainObserver.observe(document.body, { childList: true, subtree: true }); // Handle SPA navigation (YouTube's internal page changes without full reload) // This observer watches for URL changes, which often indicate a new video load. let lastUrl = location.href; const urlChangeObserver = new MutationObserver(() => { if (location.href !== lastUrl) { lastUrl = location.href; console.log('YouTube Elapsed/Remaining Time Toggle: URL changed, re-initializing.'); // Clean up any existing custom span from the previous video/page const existingSpan = document.querySelector('.yt-custom-time-display'); if (existingSpan) { existingSpan.remove(); } // Reset native time elements' styles in case they were hidden by a previous run const nativeCurrentTime = document.querySelector('.ytp-time-current'); if (nativeCurrentTime) nativeCurrentTime.style.display = ''; const nativeSeparator = document.querySelector('.ytp-time-separator'); if (nativeSeparator) nativeSeparator.style.display = ''; // Re-start the main observer for the new page load mainObserver.disconnect(); mainObserver.observe(document.body, { childList: true, subtree: true }); // Also try immediate initialization for the new page initializePlayer(); } }); urlChangeObserver.observe(document, { subtree: true, childList: true }); // Initial check on page load if elements are already present when the script first runs if (document.readyState === 'complete' || document.readyState === 'interactive') { initializePlayer(); } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址