// ==UserScript==
// @name SubsPlease Enhanced Image Previews
// @namespace http://tampermonkey.net/
// @version 1.0
// @description Show image previews next to the anime titles with advanced Tampermonkey settings
// @author dr.bobo0
// @license MIT
// @match https://subsplease.org/
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @grant GM_addStyle
// @grant GM_getResourceText
// @grant GM_addElement
// ==/UserScript==
(function() {
'use strict';
// Configuration object with Tampermonkey-backed settings
const CONFIG = {
imageWidth: GM_getValue('imageWidth', 100),
makeSquare: GM_getValue('makeSquare', false),
enableHoverEffects: GM_getValue('enableHoverEffects', true),
saveSettings(key, value) {
GM_setValue(key, value);
this[key] = value;
}
};
// Optimized update function with better transitions
function updateImagesOnPage() {
const images = document.querySelectorAll('.has-image img');
if (!images.length) return;
requestAnimationFrame(() => {
images.forEach(img => {
// Reset any previously set dimensions
img.removeAttribute('style');
// Apply new styles with smoother transitions
img.style.cssText = `
width: ${CONFIG.imageWidth}px;
height: ${CONFIG.makeSquare ? `${CONFIG.imageWidth}px` : 'auto'};
cursor: pointer;
transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 4px;
object-fit: ${CONFIG.makeSquare ? 'cover' : 'contain'};
max-width: none;
will-change: transform, width, height;
backface-visibility: hidden;
transform: translateZ(0);
`;
if (CONFIG.enableHoverEffects) {
const applyHoverEffect = () => {
img.style.transform = 'scale(1.1) translateZ(0)';
img.style.boxShadow = '0 4px 6px rgba(0,0,0,0.1)';
};
const removeHoverEffect = () => {
img.style.transform = 'scale(1) translateZ(0)';
img.style.boxShadow = 'none';
};
img.addEventListener('mouseenter', applyHoverEffect);
img.addEventListener('mouseleave', removeHoverEffect);
// Store event listeners for cleanup
img._hoverListeners = {
enter: applyHoverEffect,
leave: removeHoverEffect
};
} else if (img._hoverListeners) {
// Clean up old listeners
img.removeEventListener('mouseenter', img._hoverListeners.enter);
img.removeEventListener('mouseleave', img._hoverListeners.leave);
delete img._hoverListeners;
}
// Update the containing cell width with transition
const cell = img.parentElement;
if (cell && cell.tagName === 'TD') {
cell.style.cssText = `
padding-right: 10px;
vertical-align: middle;
width: ${CONFIG.imageWidth + 10}px;
transition: width 0.15s cubic-bezier(0.4, 0, 0.2, 1);
`;
}
});
});
}
function createSettingsUI() {
// Store original values when opening settings
const originalValues = {
imageWidth: CONFIG.imageWidth,
makeSquare: CONFIG.makeSquare,
enableHoverEffects: CONFIG.enableHoverEffects
};
GM_addStyle(`
#subsplease-settings-container {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: #1e1e1e;
color: #fff;
border: 2px solid #333;
border-radius: 10px;
padding: 20px;
width: 400px;
max-width: 90%;
box-shadow: 0 4px 6px rgba(0,0,0,0.4);
z-index: 10000;
}
#subsplease-settings-container h2 {
margin-top: 0;
border-bottom: 1px solid #444;
padding-bottom: 10px;
}
.setting-row {
display: flex;
align-items: center;
margin-bottom: 15px;
gap: 10px;
}
.setting-row label {
flex-grow: 1;
}
#preview-image {
max-width: 100%;
border-radius: 4px;
margin-top: 10px;
}
.button-row {
display: flex;
justify-content: space-between;
margin-top: 15px;
}
.button-row button {
background: #444;
color: #fff;
border: none;
border-radius: 4px;
padding: 8px 12px;
cursor: pointer;
transition: background 0.3s;
}
.button-row button:hover {
background: #555;
}
#image-width-slider {
width: 200px;
cursor: pointer;
}
`);
const settingsContainer = document.createElement('div');
settingsContainer.id = 'subsplease-settings-container';
settingsContainer.innerHTML = `
<h2>SubsPlease Image Preview Settings</h2>
<div class="setting-row">
<label for="image-width-slider">Image Width: <span id="width-value">${CONFIG.imageWidth}px</span></label>
<input type="range" id="image-width-slider" min="50" max="300" step="10" value="${CONFIG.imageWidth}">
</div>
<div class="setting-row">
<label for="square-images-toggle">Square Images</label>
<input type="checkbox" id="square-images-toggle" ${CONFIG.makeSquare ? 'checked' : ''}>
</div>
<div class="setting-row">
<label for="hover-effects-toggle">Hover Effects</label>
<input type="checkbox" id="hover-effects-toggle" ${CONFIG.enableHoverEffects ? 'checked' : ''}>
</div>
<div class="button-row">
<button id="save-settings">Save</button>
<button id="close-settings">Cancel</button>
</div>
`;
document.body.appendChild(settingsContainer);
const widthSlider = document.getElementById('image-width-slider');
const widthValue = document.getElementById('width-value');
const squareToggle = document.getElementById('square-images-toggle');
const hoverToggle = document.getElementById('hover-effects-toggle');
const saveButton = document.getElementById('save-settings');
const closeButton = document.getElementById('close-settings');
// Optimized slider update with debounce
const smoothUpdate = debounce((value) => {
CONFIG.imageWidth = parseInt(value);
updateImagesOnPage();
}, 10);
// Show live changes on slider and toggles
widthSlider.addEventListener('input', (e) => {
widthValue.textContent = `${e.target.value}px`;
smoothUpdate(e.target.value);
});
squareToggle.addEventListener('change', () => {
CONFIG.makeSquare = squareToggle.checked;
updateImagesOnPage();
});
hoverToggle.addEventListener('change', () => {
CONFIG.enableHoverEffects = hoverToggle.checked;
updateImagesOnPage();
});
saveButton.addEventListener('click', () => {
CONFIG.saveSettings('imageWidth', parseInt(widthSlider.value));
CONFIG.saveSettings('makeSquare', squareToggle.checked);
CONFIG.saveSettings('enableHoverEffects', hoverToggle.checked);
settingsContainer.remove();
});
closeButton.addEventListener('click', () => {
// Restore original values when canceling
CONFIG.imageWidth = originalValues.imageWidth;
CONFIG.makeSquare = originalValues.makeSquare;
CONFIG.enableHoverEffects = originalValues.enableHoverEffects;
// Update the page with restored values
updateImagesOnPage();
// Remove the settings container
settingsContainer.remove();
});
}
GM_registerMenuCommand('Configure Image Previews', createSettingsUI);
const injectImages = debounce(() => {
const rows = document.querySelectorAll(".frontpage-releases-container tr:not(.has-image)");
rows.forEach(row => {
try {
const name = row.querySelector(".release-item a");
const { previewImage } = name.dataset;
if (!previewImage) return;
const img = document.createElement("img");
img.src = previewImage;
img.alt = name.textContent + " preview";
img.style.cssText = `
width: ${CONFIG.imageWidth}px;
height: ${CONFIG.makeSquare ? `${CONFIG.imageWidth}px` : 'auto'};
cursor: pointer;
transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 4px;
object-fit: ${CONFIG.makeSquare ? 'cover' : 'contain'};
max-width: none;
will-change: transform, width, height;
backface-visibility: hidden;
transform: translateZ(0);
`;
if (CONFIG.enableHoverEffects) {
img.addEventListener('mouseenter', () => {
img.style.transform = 'scale(1.1) translateZ(0)';
img.style.boxShadow = '0 4px 6px rgba(0,0,0,0.1)';
});
img.addEventListener('mouseleave', () => {
img.style.transform = 'scale(1) translateZ(0)';
img.style.boxShadow = 'none';
});
}
img.addEventListener('click', (e) => {
e.preventDefault();
window.location.href = name.href;
});
const td = document.createElement("td");
td.style.cssText = `
padding-right: 10px;
vertical-align: middle;
width: ${CONFIG.imageWidth + 10}px;
transition: width 0.15s cubic-bezier(0.4, 0, 0.2, 1);
`;
td.appendChild(img);
row.insertBefore(td, row.querySelector("td:first-child"));
row.classList.add('has-image');
const info = row.querySelector(".release-item-time");
if (info) {
info.style.verticalAlign = "top";
}
} catch (error) {
console.warn('Error processing row:', error);
}
});
}, 300);
injectImages();
const loadMoreButton = document.querySelector("#latest-load-more span");
if (loadMoreButton) {
loadMoreButton.addEventListener('click', injectImages);
}
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type === 'childList') {
injectImages();
break;
}
}
});
const container = document.querySelector(".frontpage-releases-container");
if (container) {
observer.observe(container, {
childList: true,
subtree: true
});
}
function debounce(func, wait) {
let timeout;
return function(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
})();