// ==UserScript==
// @name SubDL Image Preview
// @namespace http://tampermonkey.net/
// @version 2.2
// @description Display image previews on subdl.com .
// @author dr.bobo0
// @match https://subdl.com/u/*
// @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB8AAAAgCAMAAADdXFNzAAAAXVBMVEVHcEz/7ir/7ir/7ir/7ir/7ir/7ir/7in/7ir/7ir/7ir/7ir/7Sr/7ir/7ir/7ir/9Sn/8iopKjMxMTMdIDMKEDSDfDBkXzH/+inBtS3bzSzSxC1GRDLs3SuZkC8BXe1rAAAAD3RSTlMAmpG5qyvRFvBGY9wGycP/EwDKAAABG0lEQVQokX2Ti5KDIAxFUSmgtg1P6/v/P3NFB0y03TvjjPFAIjeEsSzZ8FaBankj2V21gFOivtDnG6jeT4wfcNcD5f6CAer/duMMPzDAgXmOtYnSJsWcZl982DXmBbFClXd31lm7PR+dPlW4euRd12G+/UGDue11PwfEG1Zg7hdjpumsDwUTmLvZwHaC84SCqfxuxrDV7okDCpujh+D8ShcQ8/rVuzBowhWO9Me6sKD6ir1IOpidcyh8sZJgs3jiT4l7r7UGM3nMa+qv1aafqb9neyMfpxHz2GCJuPPeO3fy/aKnDpgh+KiwJl4cFyhZYKZhV8IqDU4+3SGS/fcFRgMg1Y0qOoTFBRfX+ZQcUf5tglldVqIVVYmH9w/WzDC9Fj6LqQAAAABJRU5ErkJggg==
// @grant none
// ==/UserScript==
(function() {
'use strict';
console.log("UserScript started.");
const storagePrefix = "subdl_image_cache_";
const maxCacheAge = 7 * 24 * 60 * 60 * 1000; // 7 days in milliseconds
const exclusionList = [
'/', // Home
'/panel',
'/panel/my-subtitles',
'/panel/account',
'/panel/api',
'/latest',
'/popular',
'https://t.me/subdl_com', // External link
'/ads', // Advertise link
'/api-doc', // API documentation
'/panel/logout',
'/login', // Login page
'#', // Placeholder links like Privacy Policy
'/signup' // Signup page
];
function clearOldCache() {
console.log("Clearing old cache entries.");
const now = Date.now();
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && key.startsWith(storagePrefix)) {
try {
const item = JSON.parse(localStorage.getItem(key));
if (now - item.timestamp > maxCacheAge) {
localStorage.removeItem(key);
console.log(`Removed old cache entry: ${key}`);
}
} catch (e) {
console.error(`Error parsing cache item: ${key}`, e);
localStorage.removeItem(key);
}
}
}
}
function addPreviewToLinks() {
console.log("Adding preview to links.");
const links = document.querySelectorAll('a[href*="/s/info/"]');
let linksFound = 0;
links.forEach(link => {
if (shouldAddPreview(link)) {
linksFound++;
if (!link.dataset.hasEventListener) {
console.log("Adding event listener to link:", link.href);
link.dataset.hasEventListener = 'true';
link.addEventListener("mouseover", function() {
console.log("Mouseover event triggered for link:", this.href);
let previewContainer = createPreviewContainer();
document.body.appendChild(previewContainer);
showLoadingSpinner(previewContainer);
fetchImage(this.href, previewContainer);
let removeMousemoveListener = addMousemoveListener(previewContainer);
handleMouseout(link, previewContainer, removeMousemoveListener);
handleClick(link, previewContainer, removeMousemoveListener);
});
} else {
console.log("Event listener already added for link:", link.href);
}
}
});
if (linksFound === 0) {
console.warn("No suitable links found.");
}
}
function shouldAddPreview(link) {
const href = link.href;
// Check if the link is in the exclusion list
for (const exclusion of exclusionList) {
if (href.endsWith(exclusion)) {
return false;
}
}
const pattern = /subdl.com/; // General pattern to match subdl.com links
return pattern.test(href);
}
function createPreviewContainer() {
console.log("Creating preview container.");
let previewContainer = document.createElement("div");
Object.assign(previewContainer.style, {
position: "fixed",
display: "none",
transition: "opacity 0.1s ease-in-out",
opacity: 0,
width: "154px",
height: "231px",
overflow: "hidden",
zIndex: 1000,
borderRadius: "8px",
boxShadow: "0 4px 8px rgba(0,0,0,0.2)",
backgroundColor: "#ffffff"
});
console.log("Preview container created:", previewContainer);
return previewContainer;
}
function showLoadingSpinner(previewContainer) {
console.log("Showing loading spinner.");
previewContainer.innerHTML = `
<div style="display: flex; justify-content: center; align-items: center; width: 100%; height: 100%; background-color: #f0f0f0;">
<div style="width: 40px; height: 40px; border: 4px solid #333; border-top: 4px solid #999; border-radius: 50%; animation: spin 1s linear infinite;"></div>
</div>
<style>
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
`;
previewContainer.style.display = "block";
previewContainer.style.opacity = 1;
}
function fetchImage(url, previewContainer) {
console.log("Fetching image for URL:", url);
const cacheKey = storagePrefix + url;
let cachedImage = localStorage.getItem(cacheKey);
if (cachedImage) {
try {
const parsedCache = JSON.parse(cachedImage);
if (Date.now() - parsedCache.timestamp < maxCacheAge) {
console.log(`Image found in cache for ${url}`);
setImage(previewContainer, parsedCache.src);
return;
} else {
console.log(`Cached image for ${url} is too old, fetching new one`);
localStorage.removeItem(cacheKey);
}
} catch (e) {
console.error(`Error parsing cached image for ${url}`, e);
localStorage.removeItem(cacheKey);
}
}
console.log(`Image not in cache, fetching from network for ${url}`);
fetch(url)
.then(response => response.text())
.then(html => {
console.log("Fetched HTML for URL:", url);
let parser = new DOMParser();
let doc = parser.parseFromString(html, 'text/html');
let preview = doc.querySelector("div.select-none img"); // New image selector
if (preview) {
let src = preview.getAttribute("src");
console.log(`Image fetched successfully for ${url}`);
setImage(previewContainer, src);
try {
localStorage.setItem(cacheKey, JSON.stringify({
src: src,
timestamp: Date.now()
}));
} catch (e) {
console.error(`Failed to cache image for ${url}:`, e);
clearOldCache(); // Attempt to free up space
}
} else {
console.log(`Image not found in the fetched HTML for ${url}`);
setError(previewContainer, "Image not found.");
}
})
.catch(error => {
console.error(`Failed to fetch image for ${url}: ${error}`);
setError(previewContainer, "Failed to load image.");
});
}
function setImage(previewContainer, src) {
console.log("Setting image for preview container:", src);
previewContainer.innerHTML = `<img style="width: 100%; height: 100%; object-fit: cover;" src="${src}"/>`;
previewContainer.style.display = "block";
previewContainer.style.opacity = 1;
console.log("Image displayed in preview container.");
}
function setError(previewContainer, message) {
console.log("Setting error message for preview container:", message);
previewContainer.innerHTML = `
<div style="display: flex; justify-content: center; align-items: center; width: 100%; height: 100%; color: red; font-weight: bold; text-align: center;">
${message}
</div>
`;
previewContainer.style.display = "block";
previewContainer.style.opacity = 1;
console.log("Error message displayed in preview container.");
}
function addMousemoveListener(previewContainer) {
console.log("Adding mousemove listener for preview container.");
function movePreview(event) {
previewContainer.style.top = event.clientY + 20 + "px";
previewContainer.style.left = event.clientX + 20 + "px";
if (event.clientX + previewContainer.offsetWidth + 20 > window.innerWidth) {
previewContainer.style.left = window.innerWidth - previewContainer.offsetWidth - 20 + "px";
}
if (event.clientY + previewContainer.offsetHeight + 20 > window.innerHeight) {
previewContainer.style.top = window.innerHeight - previewContainer.offsetHeight - 20 + "px";
}
}
document.addEventListener("mousemove", movePreview);
console.log("Mousemove listener added.");
return () => {
document.removeEventListener("mousemove", movePreview);
console.log("Mousemove listener removed.");
};
}
function handleMouseout(link, previewContainer, removeMousemoveListener) {
console.log("Adding mouseout listener for link:", link.href);
link.addEventListener("mouseout", function() {
console.log("Mouseout event triggered for link:", link.href);
cleanupPreview(previewContainer, removeMousemoveListener);
});
}
function handleClick(link, previewContainer, removeMousemoveListener) {
console.log("Adding click listener for link:", link.href);
link.addEventListener("click", function() {
console.log("Click event triggered for link:", link.href);
cleanupPreview(previewContainer, removeMousemoveListener);
});
}
function cleanupPreview(previewContainer, removeMousemoveListener) {
previewContainer.style.opacity = 0;
setTimeout(() => {
previewContainer.style.display = "none";
previewContainer.remove();
removeMousemoveListener();
console.log("Preview container removed.");
}, 200);
}
// Clear old cache entries and set up the DOM mutation observer
clearOldCache();
addPreviewToLinks();
const observer = new MutationObserver(() => {
console.log("DOM mutation detected, adding preview to new links.");
addPreviewToLinks();
});
observer.observe(document.body, { childList: true, subtree: true });
})();