// ==UserScript==
// @name Vendor (Optimized)
// @namespace https://ecsr.io/
// @version 2.0.0
// @description Custom styling panel for ecsr.io - Optimized version
// @author ZyyScripts (https://github.com/ZyyScripts) - Optimized by Cascade
// @match https://ecsr.io/*
// @icon https://i.imgur.com/lKe4ks1.png
// @grant none
// @license MIT
// ==/UserScript==
(() => {
'use strict';
// Configuration with validation
const defaultConfig = {
bgMedia: '',
font: 'Arial, sans-serif',
radius: 0,
noAds: false,
noAlerts: false,
panelPosition: { x: 20, y: 20 }
};
let config = {};
let observers = [];
let eventListeners = [];
// Safe config loading with validation
function loadConfig() {
try {
const stored = localStorage.getItem('vendor');
config = Object.assign({}, defaultConfig, stored ? JSON.parse(stored) : {});
// Validate config values
config.radius = Math.max(0, Math.min(50, Number(config.radius) || 0));
config.font = typeof config.font === 'string' ? config.font : defaultConfig.font;
config.panelPosition = config.panelPosition || defaultConfig.panelPosition;
} catch (e) {
console.warn('Vendor: Failed to load config, using defaults:', e);
config = { ...defaultConfig };
}
}
function saveConfig() {
try {
localStorage.setItem('vendor', JSON.stringify(config));
} catch (e) {
console.error('Vendor: Failed to save config:', e);
}
}
// Debounced save function
const debouncedSave = debounce(saveConfig, 300);
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// CSS injection with better performance
function injectStyle() {
const styleId = 'vendor-style';
let style = document.getElementById(styleId);
if (!style) {
style = document.createElement('style');
style.id = styleId;
document.head.appendChild(style);
}
// Use CSS custom properties for better performance
style.textContent = `
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&family=VT323&family=Share+Tech+Mono&family=Orbitron&family=Pixelify+Sans:wght@400;700&display=swap');
:root {
--vendor-font: ${CSS.escape(config.font)};
--vendor-radius: ${config.radius}px;
--vendor-bg: ${config.bgMedia ? `url("${CSS.escape(config.bgMedia)}")` : 'none'};
}
html, body {
font-family: var(--vendor-font) !important;
${config.bgMedia ? `
background: var(--vendor-bg) no-repeat center center fixed !important;
background-size: cover !important;
` : ''}
}
body *:not(#vendor-panel, #vendor-panel *, .vendor-exclude) {
font-family: var(--vendor-font) !important;
border-radius: var(--vendor-radius) !important;
}
#vendor-panel {
position: fixed;
top: ${config.panelPosition.y}%;
left: ${config.panelPosition.x}%;
width: min(500px, 90vw);
max-height: 80vh;
overflow-y: auto;
background: rgba(51, 51, 51, 0.95);
backdrop-filter: blur(10px);
border: 2px solid #000;
padding: 0;
z-index: 99999;
color: #fff;
font-family: var(--vendor-font);
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
border-radius: calc(var(--vendor-radius) + 5px);
transition: all 0.3s ease;
}
#vendor-panel .header {
background: linear-gradient(135deg, #000, #333);
padding: 12px 15px;
font-weight: bold;
font-size: 16px;
text-align: center;
color: #fff;
cursor: move;
user-select: none;
border-radius: calc(var(--vendor-radius) + 3px) calc(var(--vendor-radius) + 3px) 0 0;
}
#vendor-panel .content {
padding: 15px;
}
#vendor-panel .row {
margin-bottom: 12px;
display: flex;
flex-direction: column;
gap: 5px;
}
#vendor-panel label {
font-weight: 500;
font-size: 14px;
}
#vendor-panel select,
#vendor-panel input[type="range"],
#vendor-panel input[type="file"] {
width: 100%;
padding: 8px;
border: 1px solid #555;
border-radius: 4px;
background: #222;
color: #fff;
font-family: inherit;
}
#vendor-panel input[type="checkbox"] {
margin-right: 8px;
transform: scale(1.2);
}
#vendor-panel .actions {
display: flex;
gap: 10px;
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #555;
}
#vendor-panel button {
flex: 1;
padding: 10px;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
transition: all 0.2s ease;
}
#vendor-panel #save {
background: #4CAF50;
color: white;
}
#vendor-panel #save:hover {
background: #45a049;
}
#vendor-panel #reset {
background: #f44336;
color: white;
}
#vendor-panel #reset:hover {
background: #da190b;
}
#vendor-panel #close-panel {
float: right;
cursor: pointer;
padding: 0 5px;
border-radius: 3px;
transition: background 0.2s ease;
}
#vendor-panel #close-panel:hover {
background: rgba(255, 255, 255, 0.2);
}
.vendor-range-value {
font-size: 12px;
color: #ccc;
text-align: right;
}
/* Hide elements based on config */
${config.noAds ? `
[class*="adWrapper"],
[class*="ad-"],
.advertisement,
[data-ad] {
display: none !important;
}
` : ''}
${config.noAlerts ? `
.alertBg-0-2-19,
[class*="alert"],
.notification-popup {
display: none !important;
}
` : ''}
/* Responsive design */
@media (max-width: 768px) {
#vendor-panel {
width: 95vw;
left: 2.5% !important;
top: 10% !important;
}
}
`;
}
// Optimized DOM updates using CSS custom properties
function updateStyles() {
const root = document.documentElement;
root.style.setProperty('--vendor-font', config.font);
root.style.setProperty('--vendor-radius', `${config.radius}px`);
root.style.setProperty('--vendor-bg', config.bgMedia ? `url("${config.bgMedia}")` : 'none');
}
// Enhanced panel creation with better event handling
function createPanel() {
if (document.getElementById('vendor-panel')) return;
const panel = document.createElement('div');
panel.id = 'vendor-panel';
panel.style.display = 'none';
panel.className = 'vendor-exclude';
const fontOptions = [
'Arial, sans-serif',
'Verdana, sans-serif',
'Tahoma, sans-serif',
'Trebuchet MS, sans-serif',
'Courier New, monospace',
'Georgia, serif',
"'Press Start 2P', monospace",
"'VT323', monospace",
"'Share Tech Mono', monospace",
"'Orbitron', sans-serif",
"'Pixelify Sans', cursive"
];
panel.innerHTML = `
<div class="header">
Vendor
<span id="close-panel" title="Close Panel">✖</span>
</div>
<div class="content">
<div class="row">
<label for="font-select">Font Family:</label>
<select id="font-select">
${fontOptions.map(font =>
`<option value="${font}" ${config.font === font ? 'selected' : ''}>
${font.replace(/['"]/g, '').replace(/,.*/, '')}
</option>`
).join('')}
</select>
</div>
<div class="row">
<label for="radius">Border Radius:</label>
<input id="radius" type="range" min="0" max="50" value="${config.radius}" step="1">
<div class="vendor-range-value">${config.radius}px</div>
</div>
<div class="row">
<label>
<input id="ads" type="checkbox" ${config.noAds ? 'checked' : ''}>
Hide Advertisements
</label>
</div>
<div class="row">
<label>
<input id="alerts" type="checkbox" ${config.noAlerts ? 'checked' : ''}>
Hide Alert Notifications
</label>
</div>
<div class="row">
<label for="bg-file">Background Image:</label>
<input id="bg-file" type="file" accept="image/*" aria-describedby="bg-help">
<small id="bg-help" style="color: #ccc; font-size: 12px;">
Supports JPG, PNG, GIF, WebP (max 5MB)
</small>
</div>
<div class="actions">
<button id="save" type="button">Apply Changes</button>
<button id="reset" type="button">Reset to Default</button>
</div>
</div>
`;
document.body.appendChild(panel);
setupPanelEvents(panel);
}
// Centralized event handling with cleanup
function setupPanelEvents(panel) {
const elements = {
closeBtn: panel.querySelector('#close-panel'),
fontSelect: panel.querySelector('#font-select'),
radiusInput: panel.querySelector('#radius'),
radiusValue: panel.querySelector('.vendor-range-value'),
adsCheckbox: panel.querySelector('#ads'),
alertsCheckbox: panel.querySelector('#alerts'),
bgFileInput: panel.querySelector('#bg-file'),
saveBtn: panel.querySelector('#save'),
resetBtn: panel.querySelector('#reset'),
header: panel.querySelector('.header')
};
// Close panel
const closeHandler = () => panel.style.display = 'none';
elements.closeBtn.addEventListener('click', closeHandler);
eventListeners.push({ element: elements.closeBtn, event: 'click', handler: closeHandler });
// Font selection with validation
const fontHandler = (e) => {
const selectedFont = e.target.value;
if (fontOptions.includes(selectedFont)) {
config.font = selectedFont;
updateStyles();
debouncedSave();
}
};
elements.fontSelect.addEventListener('change', fontHandler);
eventListeners.push({ element: elements.fontSelect, event: 'change', handler: fontHandler });
// Radius with live preview and validation
const radiusHandler = (e) => {
const value = Math.max(0, Math.min(50, parseInt(e.target.value) || 0));
config.radius = value;
elements.radiusValue.textContent = `${value}px`;
updateStyles();
debouncedSave();
};
elements.radiusInput.addEventListener('input', radiusHandler);
eventListeners.push({ element: elements.radiusInput, event: 'input', handler: radiusHandler });
// Checkboxes
const adsHandler = (e) => {
config.noAds = e.target.checked;
debouncedSave();
};
elements.adsCheckbox.addEventListener('change', adsHandler);
eventListeners.push({ element: elements.adsCheckbox, event: 'change', handler: adsHandler });
const alertsHandler = (e) => {
config.noAlerts = e.target.checked;
debouncedSave();
};
elements.alertsCheckbox.addEventListener('change', alertsHandler);
eventListeners.push({ element: elements.alertsCheckbox, event: 'change', handler: alertsHandler });
// File upload with validation and error handling
const fileHandler = (e) => {
const file = e.target.files?.[0];
if (!file) return;
// Validate file
const maxSize = 100 * 3840 * 2160; // 5MB
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (file.size > maxSize) {
alert('File too large. Please select an image under 5MB.');
e.target.value = '';
return;
}
if (!allowedTypes.includes(file.type)) {
alert('Invalid file type. Please select a JPG, PNG, GIF, or WebP image.');
e.target.value = '';
return;
}
const reader = new FileReader();
reader.onload = (ev) => {
try {
config.bgMedia = ev.target.result;
updateStyles();
debouncedSave();
} catch (error) {
console.error('Vendor: Failed to process image:', error);
alert('Failed to process image. Please try a different file.');
}
};
reader.onerror = () => {
console.error('Vendor: Failed to read file');
alert('Failed to read file. Please try again.');
};
reader.readAsDataURL(file);
};
elements.bgFileInput.addEventListener('change', fileHandler);
eventListeners.push({ element: elements.bgFileInput, event: 'change', handler: fileHandler });
// Save and reset with user feedback
const saveHandler = () => {
try {
injectStyle();
updateStyles();
saveConfig();
// Visual feedback
elements.saveBtn.textContent = 'Saved!';
elements.saveBtn.style.background = '#2196F3';
setTimeout(() => {
elements.saveBtn.textContent = 'Apply Changes';
elements.saveBtn.style.background = '#4CAF50';
}, 1000);
// Reload only if necessary (ads/alerts changed)
if (config.noAds || config.noAlerts) {
setTimeout(() => location.reload(), 1200);
}
} catch (error) {
console.error('Vendor: Save failed:', error);
alert('Failed to save settings. Please try again.');
}
};
elements.saveBtn.addEventListener('click', saveHandler);
eventListeners.push({ element: elements.saveBtn, event: 'click', handler: saveHandler });
const resetHandler = () => {
if (confirm('Reset all settings to default? This will reload the page.')) {
try {
localStorage.removeItem('vendor');
config = { ...defaultConfig };
updateStyles();
injectStyle();
location.reload();
} catch (error) {
console.error('Vendor: Reset failed:', error);
alert('Failed to reset settings. Please try again.');
}
}
};
elements.resetBtn.addEventListener('click', resetHandler);
eventListeners.push({ element: elements.resetBtn, event: 'click', handler: resetHandler });
// Enhanced draggable functionality
setupDraggable(panel, elements.header);
}
// Improved draggable with position persistence
function setupDraggable(panel, header) {
let isDragging = false;
let startX, startY, startLeft, startTop;
const mouseDownHandler = (e) => {
isDragging = true;
startX = e.clientX;
startY = e.clientY;
startLeft = panel.offsetLeft;
startTop = panel.offsetTop;
header.style.cursor = 'grabbing';
document.body.style.userSelect = 'none';
};
const mouseMoveHandler = (e) => {
if (!isDragging) return;
e.preventDefault();
const deltaX = e.clientX - startX;
const deltaY = e.clientY - startY;
let newLeft = startLeft + deltaX;
let newTop = startTop + deltaY;
// Constrain to viewport
const maxLeft = window.innerWidth - panel.offsetWidth;
const maxTop = window.innerHeight - panel.offsetHeight;
newLeft = Math.max(0, Math.min(newLeft, maxLeft));
newTop = Math.max(0, Math.min(newTop, maxTop));
panel.style.left = `${newLeft}px`;
panel.style.top = `${newTop}px`;
};
const mouseUpHandler = () => {
if (isDragging) {
isDragging = false;
header.style.cursor = 'move';
document.body.style.userSelect = '';
// Save position as percentage
config.panelPosition = {
x: (panel.offsetLeft / window.innerWidth) * 100,
y: (panel.offsetTop / window.innerHeight) * 100
};
debouncedSave();
}
};
header.addEventListener('mousedown', mouseDownHandler);
document.addEventListener('mousemove', mouseMoveHandler);
document.addEventListener('mouseup', mouseUpHandler);
eventListeners.push(
{ element: header, event: 'mousedown', handler: mouseDownHandler },
{ element: document, event: 'mousemove', handler: mouseMoveHandler },
{ element: document, event: 'mouseup', handler: mouseUpHandler }
);
}
// Optimized navigation tab injection using MutationObserver
function addNavTab() {
if (document.getElementById('vendor-tab')) return;
const selectors = [
'.navContainer-0-2-2 .row .col-0-2-15 .row',
'.row:has(.linkEntry-0-2-13)',
'nav .row'
];
for (const selector of selectors) {
try {
const navRow = document.querySelector(selector);
if (navRow) {
const tab = document.createElement('div');
tab.innerHTML = `<a id="vendor-tab" class="linkEntry-0-2-13 nav-link pt-0 vendor-exclude" href="#" title="Open Vendor Panel">Vendor</a>`;
navRow.appendChild(tab);
const clickHandler = (e) => {
e.preventDefault();
const panel = document.getElementById('vendor-panel');
if (panel) {
const isVisible = panel.style.display !== 'none';
panel.style.display = isVisible ? 'none' : 'block';
}
};
const tabLink = tab.querySelector('#vendor-tab');
tabLink.addEventListener('click', clickHandler);
eventListeners.push({ element: tabLink, event: 'click', handler: clickHandler });
return true;
}
} catch (e) {
console.warn('Vendor: Failed to find nav with selector:', selector);
}
}
return false;
}
// Comprehensive logo replacement for all instances of ECSR logo URL
// Optimized initialization using MutationObserver
function observeDOM() {
const observer = new MutationObserver((mutations) => {
let shouldCheck = false;
for (const mutation of mutations) {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
shouldCheck = true;
break;
}
}
if (shouldCheck) {
addNavTab();
replaceLogo();
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
observers.push(observer);
}
// Cleanup function
function cleanup() {
// Remove event listeners
eventListeners.forEach(({ element, event, handler }) => {
try {
element.removeEventListener(event, handler);
} catch (e) {
console.warn('Vendor: Failed to remove event listener:', e);
}
});
eventListeners = [];
// Disconnect observers
observers.forEach(observer => {
try {
observer.disconnect();
} catch (e) {
console.warn('Vendor: Failed to disconnect observer:', e);
}
});
observers = [];
}
// Enhanced initialization
function init() {
try {
loadConfig();
injectStyle();
updateStyles();
createPanel();
// Initial setup
addNavTab();
// Set up DOM observation for dynamic content
observeDOM();
// Cleanup on page unload
window.addEventListener('beforeunload', cleanup);
console.log('Vendor: Successfully initialized');
} catch (error) {
console.error('Vendor: Initialization failed:', error);
}
}
// Robust initialization
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
// Use requestAnimationFrame for better performance
requestAnimationFrame(init);
}
// Global reference for debugging (development only)
if (typeof window !== 'undefined') {
window.vendorDebug = {
config,
saveConfig,
loadConfig,
cleanup,
version: '2.0.0'
};
}
})();