Left-aligned progress indicators with fixed thumbnail height
当前为
// ==UserScript==
// @name YouTube Universal Progress Tracker
// @namespace http://tampermonkey.net/
// @version 3.2
// @description Left-aligned progress indicators with fixed thumbnail height
// @author ikigaiDH
// @match https://www.youtube.com/*
// @grant none
// @license GPL-3.0-only
// ==/UserScript==
(function() {
'use strict';
const STORAGE_KEY = 'yt_watch_history';
let watchHistory = JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}');
// Universal video ID extractor
const getVideoId = (element) => {
try {
const link = element.closest('a') || element.querySelector('a');
if (!link) return null;
const url = new URL(link.href);
return url.searchParams.get('v') ||
url.pathname.split('/watch/')[1]?.split('?')[0] ||
url.pathname.split('/')[2];
} catch {
return null;
}
};
// Progress indicator creation
const createIndicator = () => {
const indicator = document.createElement('div');
Object.assign(indicator.style, {
position: 'absolute',
bottom: '4px',
left: '4px',
backgroundColor: '#cc0000',
color: 'white',
padding: '2px 6px',
borderRadius: '2px',
fontSize: '12px',
fontWeight: 'bold',
zIndex: '1000',
fontFamily: 'Roboto, Arial, sans-serif',
textTransform: 'uppercase'
});
indicator.className = 'yt-progress-indicator';
return indicator;
};
// Main update function
const updateAllThumbnails = () => {
// First, clean up any existing indicators to prevent duplicates
document.querySelectorAll('.yt-progress-indicator').forEach(ind => ind.remove());
// Process all thumbnails
document.querySelectorAll('ytd-thumbnail').forEach(thumbnail => {
// Skip thumbnails that don't have proper structure
if (!thumbnail.querySelector('#thumbnail')) return;
const videoId = getVideoId(thumbnail);
if (!videoId) return;
const percentage = watchHistory[videoId] || 0;
if (percentage > 0) {
// Create new indicator
const indicator = createIndicator();
indicator.textContent = percentage >= 100 ? '>100%' : `${Math.round(percentage)}%`;
// Find the correct container for the indicator
const overlays = thumbnail.querySelector('#overlays');
if (overlays) {
overlays.appendChild(indicator);
} else {
const img = thumbnail.querySelector('#img');
if (img) {
if (!img.style.position) {
img.style.position = 'relative';
}
img.appendChild(indicator);
}
}
}
});
};
// Video tracking with debouncing
let currentVideoId = null;
const trackVideo = () => {
const video = document.querySelector('video');
if (!video) return;
const newVideoId = new URLSearchParams(window.location.search).get('v');
if (newVideoId === currentVideoId) return;
currentVideoId = newVideoId;
const saveProgress = () => {
if (video.duration > 0) {
const percentage = (video.currentTime / video.duration) * 100;
if (percentage > (watchHistory[currentVideoId] || 0)) {
watchHistory[currentVideoId] = percentage;
localStorage.setItem(STORAGE_KEY, JSON.stringify(watchHistory));
updateAllThumbnails();
}
}
};
video.addEventListener('timeupdate', saveProgress);
};
// Enhanced observation with debouncing
let updateTimeout = null;
const observer = new MutationObserver(() => {
if (updateTimeout) {
clearTimeout(updateTimeout);
}
updateTimeout = setTimeout(() => {
updateAllThumbnails();
trackVideo();
}, 100);
});
// Initialization
window.addEventListener('load', () => {
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: false
});
updateAllThumbnails();
trackVideo();
});
// Handle YouTube navigation
document.addEventListener('yt-navigate-finish', updateAllThumbnails);
document.addEventListener('yt-page-data-updated', updateAllThumbnails);
})();