// ==UserScript==
// @name SubDL Image Preview Beside Titles
// @namespace http://tampermonkey.net/
// @version 1.4
// @description Display image previews beside subtitles on subdl.com.
// @author dr.bobo0
// @license MIT
// @match https://subdl.com/*
// @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 GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// ==/UserScript==
(function() {
'use strict';
// Default configuration with persistent storage
const DEFAULT_CONFIG = {
imageWidth: 75,
imageHeight: 112,
isSquare: false,
hideDownloadButton: false
};
// Get current settings, merging with defaults
function getSettings() {
return {
imageWidth: GM_getValue('imageWidth', DEFAULT_CONFIG.imageWidth),
imageHeight: GM_getValue('imageHeight', DEFAULT_CONFIG.imageHeight),
isSquare: GM_getValue('isSquare', DEFAULT_CONFIG.isSquare),
hideDownloadButton: GM_getValue('hideDownloadButton', DEFAULT_CONFIG.hideDownloadButton)
};
}
// Save settings
function saveSettings(settings) {
GM_setValue('imageWidth', settings.imageWidth);
GM_setValue('imageHeight', settings.imageHeight);
GM_setValue('isSquare', settings.isSquare);
GM_setValue('hideDownloadButton', settings.hideDownloadButton);
}
// Configuration function
function getImageConfig() {
const settings = getSettings();
return {
STORAGE_PREFIX: "subdl_image_cache_",
MAX_CACHE_AGE: 7 * 24 * 60 * 60 * 1000, // 7 days in milliseconds
IMAGE_STYLES: {
width: `${settings.imageWidth}px`,
height: `${settings.imageHeight}px`,
objectFit: settings.isSquare ? 'cover' : 'contain',
borderRadius: '4px',
boxShadow: '0 2px 6px rgba(0, 0, 0, 0.15)',
marginRight: '8px'
},
LINK_STYLES: {
display: 'flex',
alignItems: 'center',
gap: '8px'
}
};
}
// Utility function to safely parse JSON from localStorage
function safeJSONParse(key) {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : null;
} catch (e) {
localStorage.removeItem(key);
return null;
}
}
// Clear old cache entries
function clearOldCache() {
const CONFIG = getImageConfig();
const now = Date.now();
Object.keys(localStorage)
.filter(key => key.startsWith(CONFIG.STORAGE_PREFIX))
.forEach(key => {
const item = safeJSONParse(key);
if (!item || now - item.timestamp > CONFIG.MAX_CACHE_AGE) {
localStorage.removeItem(key);
}
});
}
// Apply styles to an element
function applyStyles(element, styles) {
Object.entries(styles).forEach(([key, value]) => {
element.style[key] = value;
});
}
// Helper function for creating elements with styles and attributes
function createElement(tag, styles = {}, attributes = {}) {
const element = document.createElement(tag);
applyStyles(element, styles);
Object.entries(attributes).forEach(([key, value]) => {
element[key] = value;
});
return element;
}
// Fetch image for a specific subtitle link
async function fetchImage(url, container) {
const CONFIG = getImageConfig();
const fullUrl = new URL(url, window.location.origin).href;
const cacheKey = CONFIG.STORAGE_PREFIX + fullUrl;
// Check cache
const cachedImage = safeJSONParse(cacheKey);
if (cachedImage && Date.now() - cachedImage.timestamp < CONFIG.MAX_CACHE_AGE) {
displayImage(cachedImage.src, container);
return;
}
// Fetch new image
try {
const response = await fetch(fullUrl);
const html = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const preview = doc.querySelector("div.select-none img");
if (preview) {
const src = preview.getAttribute("src");
if (src) {
displayImage(src, container);
// Cache the image
localStorage.setItem(cacheKey, JSON.stringify({
src: src,
timestamp: Date.now()
}));
}
} else {
console.warn(`No preview image found for ${fullUrl}`);
}
} catch (error) {
console.error(`Failed to fetch image for ${fullUrl}:`, error);
}
}
// Display image beside the title
function displayImage(src, container) {
const CONFIG = getImageConfig();
const img = document.createElement("img");
img.src = src;
img.alt = "Preview";
img.setAttribute('data-subdl-preview', 'true');
const styles = CONFIG.IMAGE_STYLES;
Object.entries(styles).forEach(([key, value]) => {
img.style[key] = value;
});
// Add error handling to use poster.jpeg if image fails to load
img.onerror = () => {
img.src = 'https://subdl.com/images/poster.jpeg';
};
container.parentElement.insertBefore(img, container);
}
// Add image previews to links
function addImagePreviews() {
const links = document.querySelectorAll('a[href^="/s/info/"]:not([data-image-preview])');
const CONFIG = getImageConfig();
links.forEach(link => {
// Mark as processed
link.setAttribute('data-image-preview', 'true');
const container = link.querySelector('h3');
if (!container) return;
// Style the link and parent
applyStyles(link, CONFIG.LINK_STYLES);
const parentDiv = link.closest('.flex');
if (parentDiv) {
applyStyles(parentDiv, {
display: 'flex',
alignItems: 'center',
gap: '12px'
});
}
// Adjust SVG icon if present
const svgIcon = link.querySelector('svg');
if (svgIcon) {
applyStyles(svgIcon, {
width: '20px',
height: '20px',
marginRight: '8px',
verticalAlign: 'middle'
});
}
// Fetch and display image
fetchImage(link.href, container);
});
}
// Elegant color palette
const COLORS = {
background: '#1a202c',
modalBg: '#2d3748',
primary: '#4299e1',
secondary: '#718096',
accent: '#48bb78',
textPrimary: '#e2e8f0',
textSecondary: '#a0aec0',
borderColor: '#4a5568'
};
// Create settings modal
function createSettingsModal() {
// Create modal backdrop using the helper function
const backdrop = createElement('div', {
position: 'fixed',
top: '0',
left: '0',
width: '100%',
height: '100%',
background: 'rgba(0,0,0,0.5)',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
zIndex: '10000',
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif'
});
// Create modal content
const modal = createElement('div', {
background: COLORS.modalBg,
padding: '30px',
borderRadius: '16px',
width: '500px',
boxShadow: '0 25px 50px -12px rgba(0,0,0,0.15)',
position: 'relative',
maxHeight: '80vh',
overflowY: 'auto'
});
// Title
const title = createElement('h2', {
color: COLORS.primary,
marginBottom: '25px',
textAlign: 'center',
fontWeight: '700',
fontSize: '1.5rem'
}, { textContent: 'Image Preview Settings' });
modal.appendChild(title);
// Current settings
const settings = getSettings();
// Preview Container
const previewContainer = createElement('div', {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
marginBottom: '25px',
background: COLORS.background,
padding: '25px',
borderRadius: '12px'
});
modal.appendChild(previewContainer);
// Preview Image
const previewImg = createElement('img', {
transition: 'all 0.3s ease',
borderRadius: '12px',
boxShadow: '0 10px 25px rgba(0,0,0,0.1)'
}, { src: 'https://via.placeholder.com/150x225?text=Preview' });
previewContainer.appendChild(previewImg);
// Settings Container
const settingsContainer = createElement('div', {
display: 'flex',
flexDirection: 'column',
gap: '25px'
});
modal.appendChild(settingsContainer);
// Image Width Slider
const widthContainer = createElement('div');
const widthLabel = createElement('label', {
display: 'flex',
justifyContent: 'space-between',
color: COLORS.textPrimary,
fontWeight: '600'
});
const widthLabelText = createElement('span', {}, { textContent: 'Image Width' });
const widthValue = createElement('span', {}, { textContent: `${settings.imageWidth}px` });
widthLabel.appendChild(widthLabelText);
widthLabel.appendChild(widthValue);
const widthSlider = createElement('input', {
width: '100%',
accentColor: COLORS.primary
}, {
type: 'range',
min: '50',
max: '200',
value: settings.imageWidth
});
// Slider and preview synchronization
function updatePreview() {
const width = widthSlider.value;
const height = width * (squareToggle.checked ? 1 : 1.5);
widthValue.textContent = `${width}px`;
previewImg.style.width = `${width}px`;
previewImg.style.height = `${height}px`;
// Live modifications to all preview images
document.querySelectorAll('img[data-subdl-preview]').forEach(img => {
img.style.width = `${width}px`;
img.style.height = `${height}px`;
});
}
widthSlider.oninput = updatePreview;
widthContainer.appendChild(widthLabel);
widthContainer.appendChild(widthSlider);
settingsContainer.appendChild(widthContainer);
// Square Images Toggle
const squareToggleContainer = createElement('div', {
display: 'flex',
alignItems: 'center',
gap: '12px'
});
const squareToggle = createElement('input', {
accentColor: COLORS.primary
}, {
type: 'checkbox',
id: 'squareImagesToggle',
checked: settings.isSquare
});
const squareToggleLabel = createElement('label', {
color: COLORS.textSecondary
}, {
htmlFor: 'squareImagesToggle',
textContent: 'Square Images'
});
// Square toggle live update
squareToggle.oninput = () => {
const width = widthSlider.value;
const height = width * (squareToggle.checked ? 1 : 1.5);
previewImg.style.height = `${height}px`;
// Live modifications to all preview images
document.querySelectorAll('img[data-subdl-preview]').forEach(img => {
img.style.height = `${height}px`;
img.style.objectFit = squareToggle.checked ? 'cover' : 'contain';
});
};
squareToggleContainer.appendChild(squareToggle);
squareToggleContainer.appendChild(squareToggleLabel);
settingsContainer.appendChild(squareToggleContainer);
// Download Button Toggle
const downloadToggleContainer = createElement('div', {
display: 'flex',
alignItems: 'center',
gap: '12px'
});
const downloadToggle = createElement('input', {
accentColor: COLORS.primary
}, {
type: 'checkbox',
id: 'hideDownloadToggle',
checked: settings.hideDownloadButton
});
const downloadToggleLabel = createElement('label', {
color: COLORS.textSecondary
}, {
htmlFor: 'hideDownloadToggle',
textContent: 'Hide Download Buttons'
});
// Add live preview for download toggle
downloadToggle.oninput = () => {
const downloadButtons = document.querySelectorAll('a[href^="https://dl.subdl.com/subtitle/"]');
downloadButtons.forEach(button => {
button.style.display = downloadToggle.checked ? 'none' : '';
});
};
downloadToggleContainer.appendChild(downloadToggle);
downloadToggleContainer.appendChild(downloadToggleLabel);
settingsContainer.appendChild(downloadToggleContainer);
// Buttons container
const buttonContainer = createElement('div', {
display: 'flex',
justifyContent: 'space-between',
marginTop: '25px',
gap: '15px'
});
// Save button
const saveButton = createElement('button', {
backgroundColor: COLORS.accent,
color: 'white',
border: 'none',
padding: '12px 20px',
borderRadius: '8px',
cursor: 'pointer',
flexGrow: '1',
fontWeight: '600'
}, { textContent: 'Save Settings' });
saveButton.onclick = () => {
const newSettings = {
imageWidth: parseInt(widthSlider.value),
imageHeight: parseInt(widthSlider.value) * (squareToggle.checked ? 1 : 1.5),
isSquare: squareToggle.checked,
hideDownloadButton: downloadToggle.checked
};
saveSettings(newSettings);
document.body.removeChild(backdrop);
// Apply download button visibility
const downloadButtons = document.querySelectorAll('a[href^="https://dl.subdl.com/subtitle/"]');
downloadButtons.forEach(button => {
button.style.display = newSettings.hideDownloadButton ? 'none' : '';
});
// Refresh previews after saving
document.querySelectorAll('img[data-subdl-preview]').forEach(img => img.remove());
document.querySelectorAll('a[data-image-preview]').forEach(link => link.removeAttribute('data-image-preview'));
addImagePreviews();
};
// Cancel button
const cancelButton = createElement('button', {
backgroundColor: COLORS.background,
color: COLORS.textSecondary,
border: `2px solid ${COLORS.background}`,
padding: '10px 20px',
borderRadius: '8px',
cursor: 'pointer',
flexGrow: '1',
fontWeight: '600'
}, { textContent: 'Cancel' });
cancelButton.onclick = () => {
document.body.removeChild(backdrop);
};
buttonContainer.appendChild(saveButton);
buttonContainer.appendChild(cancelButton);
settingsContainer.appendChild(buttonContainer);
backdrop.appendChild(modal);
document.body.appendChild(backdrop);
// Initial setup of preview
updatePreview();
}
// Throttle utility function
function throttle(func, limit) {
let lastFunc;
let lastRan;
return function() {
const context = this;
const args = arguments;
if (!lastRan) {
func.apply(context, args);
lastRan = Date.now();
} else {
clearTimeout(lastFunc);
lastFunc = setTimeout(function() {
if (Date.now() - lastRan >= limit) {
func.apply(context, args);
lastRan = Date.now();
}
}, limit - (Date.now() - lastRan));
}
};
}
// Add settings menu command
GM_registerMenuCommand('Configure Image Previews', createSettingsModal);
// Initial setup
function applyDownloadButtonVisibility() {
const settings = getSettings();
const downloadButtons = document.querySelectorAll('a[href^="https://dl.subdl.com/subtitle/"]');
downloadButtons.forEach(button => {
button.style.display = settings.hideDownloadButton ? 'none' : '';
});
}
function init() {
clearOldCache();
addImagePreviews();
applyDownloadButtonVisibility();
const throttledAddImagePreviews = throttle(() => {
addImagePreviews();
applyDownloadButtonVisibility();
}, 500);
const observer = new MutationObserver(() => {
throttledAddImagePreviews();
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
// Run the script
init();
})();