Vendor (Optimized)

Custom styling panel for ecsr.io - Optimized version

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey, το Greasemonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Userscripts για να εγκαταστήσετε αυτόν τον κώδικα.

You will need to install an extension such as Tampermonkey to install this script.

Θα χρειαστεί να εγκαταστήσετε μια επέκταση διαχείρισης κώδικα χρήστη για να εγκαταστήσετε αυτόν τον κώδικα.

(Έχω ήδη έναν διαχειριστή κώδικα χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Έχω ήδη έναν διαχειριστή στυλ χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

// ==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'
    };
  }
})();