Subtitle Uploader

Upload subtitles to any video on any website + settings panel

// ==UserScript==
// @name         Subtitle Uploader
// @namespace    http://tampermonkey.net/
// @version      1.1
// @author       md-dahshan
// @license      MIT
// @description  Upload subtitles to any video on any website + settings panel
// @match        *://*/*
// @grant        none
// ==/UserScript==

(function () {
  'use strict';

  const defaultSettings = {
    fontSize: 30,
    fontColor: '#ffffff',
    bgColor: '#000000',
    bgToggle: true,
    offsetY: 85,
    delay: 0
  };

  const cryptoAddresses = {
    Bitcoin_BTC: '1Hi4HnetFFnM2B2GzEvJDgU48yF2mSnWh8',
    BNB_BEP20: '0x1452c2ae22683dbf6133684501044d3c44f476d3',
    USDT_TRC20: 'TEjLXNrydPDRdE2n3Wmjr3TyvSuDFm8JVg',
    PAYPAL: 'paypal.me/MDDASH',
    Binance_ID: '859818212'
  };

  const settings = loadSettings();
  const style = document.createElement('style');
  document.head.appendChild(style);

  const settingsPanel = createSettingsPanel();
  const observer = new MutationObserver(addButtons);
  observer.observe(document.body, { childList: true, subtree: true });

  addButtons();
  applySettings();

  function addButtons() {
  const video = document.querySelector('video');
  if (!video) return;

  if (!document.querySelector('.subtitle-controls')) {
    const container = document.createElement('div');
    container.className = 'subtitle-controls';

    const btnUpload = document.createElement('button');
    btnUpload.textContent = '🎬';
    btnUpload.title = 'Upload Subtitle';
    btnUpload.style.cssText = btnStyle();

    const btnSettings = document.createElement('button');
    btnSettings.textContent = '⚙️';
    btnSettings.title = 'Settings';
    btnSettings.style.cssText = btnStyle();

    btnUpload.onclick = () => {
      const input = document.createElement('input');
      input.type = 'file';
      input.accept = '.vtt,.srt';
      input.style.display = 'none';

      input.onchange = () => {
        const file = input.files[0];
        if (!file) return;

        const reader = new FileReader();
        reader.onload = () => {
          let text = reader.result;

          if (file.name.endsWith('.srt')) {
            text = 'WEBVTT\n\n' + text
              .replace(/\r+/g, '')
              .replace(
                /(\d+)\n(\d{2}:\d{2}:\d{2},\d{3}) --> (\d{2}:\d{2}:\d{2},\d{3})/g,
                '$2 --> $3'
              )
              .replace(/,/g, '.');
          }

          const blob = new Blob([text], { type: 'text/vtt' });
          const url = URL.createObjectURL(blob);

          attachSubtitle(video, url);
        };
        reader.readAsText(file);
      };

      document.body.appendChild(input);
      input.click();
    };

    btnSettings.onclick = () => {
      settingsPanel.style.display = 'block';
    };

    container.appendChild(btnUpload);
    container.appendChild(btnSettings);
    document.body.appendChild(container);

    // تحديد موقع الفيديو
    const rect = video.getBoundingClientRect();
    const scrollTop = window.scrollY || document.documentElement.scrollTop;
    const scrollLeft = window.scrollX || document.documentElement.scrollLeft;

    container.style.cssText = `
      position: absolute;
      top: ${rect.top + scrollTop + 10}px;
      left: ${rect.left + scrollLeft + rect.width - 100}px;
      z-index: 99999;
      display: flex;
      overflow: hidden;
      border-radius: 30%;
      box-shadow: 0 4px 10px rgba(0,0,0,0.4);
      background: rgba(0,0,0,0.5);
    `;
  }
}

  function btnStyle() {
    return `
      background: rgba(0,0,0,0.8);
      color: #fff;
      border: none;
      width: 40px;
      height: 40px;
      font-size: 18px;
      cursor: pointer;
    `;
  }

  function createSettingsPanel() {
    const panel = document.createElement('div');
    panel.className = 'subtitle-settings-panel';
    panel.innerHTML = `
<style>
.subtitle-settings-panel {
  position: fixed;
  top: 60px;
  right: 20px;
  background: #1e1e1e;
  color: #fff;
  padding: 15px;
  border-radius: 10px;
  box-shadow: 0 4px 20px rgba(0,0,0,0.6);
  z-index: 100000; /* أعلى من الأزرار */
  display: none;
  font-size: 14px;
  width: 250px;
  font-family: Arial,sans-serif;
  max-height: 80vh;
  overflow-y: auto;
}
.subtitle-settings-panel h3 {
  margin-top:0;margin-bottom:10px;font-size:16px;color:#ddd;text-align:center;
}
.subtitle-settings-panel label {
  display: block;
  margin: 5px 0 2px;
  font-weight:bold;
  font-size:12px;
}
.subtitle-settings-panel input,
.subtitle-settings-panel select {
  width: 100%;
  margin-bottom:8px;
  padding:4px;
  border:1px solid #555;
  border-radius:4px;
  background:#2e2e2e;
  color:#fff;
}
.subtitle-settings-panel button {
  background: #4F46E5;
  color: #fff;
  border: none;
  padding:5px 10px;
  border-radius:4px;
  cursor:pointer;
  float:right;
}
.subtitle-settings-panel button:hover {
  background:#312E81;
}
</style>
<h3 style="display:flex;align-items:center;justify-content:space-between;">
  🎨 Subtitle Settings
  <span title="
Help:
- Don't upload the subtitle twice, and if you do, reload the page.
---------------
- Subtitle Delay it works, but the difference can be in milliseconds, so be a little tired 🤨.
---------------
- If any feature does not work, such as font color for example, switch to another streaming server and try again
---------------
- For any suggestion or modification, contact me [email protected] .
---------------
- Don't forget to donate, it makes a difference, even if it's just $1, (if you want of course).

"
  style="
    cursor:help;
    font-size:10px;
    color:#ccc;
  ">❔ Hover to Help</span>
</h3>


<div style="display:flex;gap:15px;align-items:center;">
  <div style="flex:1;">
    <label>Font Size</label>
    <input type="number" id="sub-font-size" value="${settings.fontSize}">
  </div>
  <div style="flex:1;">
    <label>Font Color</label>
    <input type="color" id="sub-font-color" value="${settings.fontColor}">
  </div>
</div>

<label>Background Color</label>
<div style="display:flex;gap:15px;align-items:center;margin-bottom:8px;">
  <input type="color" id="sub-bg-color" value="${settings.bgColor}">
  <label style="display:flex;align-items:center;gap:4px;margin:0;">
    <input type="checkbox" id="sub-bg-toggle" ${settings.bgToggle ? 'checked' : ''}>
  </label>
</div>


<div style="display:flex;gap:8px;align-items:center;">
<div style="flex:1;display:flex;flex-direction:Column;align-items:center;">
<label style="margin-bottom:0px;">Position</label>
  <input
    type="range"
    min="0"
    max="100"
    id="sub-offsetY"
    value="${settings.offsetY}"
    style="width:100%;accent-color:#f44336;cursor:pointer;">
</div>
  <div style="flex:0.35;">
  <span id="sub-offsetY-value" style="font-size:10px;color:#f44336;">${settings.offsetY}px</span>
</div>

  <div style="flex:1;">
    <label>Delay(ms)</label>
    <input type="number" id="sub-delay" value="${settings.delay}" step="25">
  </div>
</div>


<h3>💰 Donate Me❤️ 🫡</h3>
<label>Select Address</label>
<select id="crypto-select">
  <option value="">-- Choose --</option>
  ${Object.entries(cryptoAddresses).map(([key, value]) =>
    `<option value="${value}">${key}</option>`).join('')}
</select>
<div style="display:flex;gap:1px;margin-top:4px;align-items:center;">
  <input type="text" id="crypto-output" readonly
    style="flex:1;background:#333;color:#fff;border:none;padding:4px;"
    placeholder="Selected address">
  <button id="copy-crypto" title="Copy"
    style="background:none;border:none;color:#fff;font-size:22px;cursor:pointer;">⎘</button>
</div>



<div style="text-align:right;margin-top:5px;"><button id="sub-close">Close</button></div>
`;
    document.body.appendChild(panel);
      const offsetYInput = panel.querySelector('#sub-offsetY');
const offsetYValue = panel.querySelector('#sub-offsetY-value');

offsetYInput.addEventListener('input', () => {
  offsetYValue.textContent = offsetYInput.value + 'px';
});


    panel.querySelector('#sub-close').onclick = () => {
      panel.style.display = 'none';
    };

    panel.querySelectorAll('input').forEach(input => {
      input.addEventListener('input', () => {
        saveSettings();
        applySettings();
      });
    });

    const cryptoSelect = panel.querySelector('#crypto-select');
    const cryptoOutput = panel.querySelector('#crypto-output');
    const cryptoCopy = panel.querySelector('#copy-crypto');

    cryptoSelect.onchange = function () {
      cryptoOutput.value = this.value;
    };

    cryptoCopy.onclick = function () {
      if (!cryptoOutput.value) {
        alert('Please select an address first!');
        return;
      }
      navigator.clipboard.writeText(cryptoOutput.value).then(() => {
        alert('Address copied!');
      });
    };

    return panel;
  }

function applySettings() {
  const size = document.querySelector('#sub-font-size').value || 30;
  const color = document.querySelector('#sub-font-color').value || '#fff';
  const bg = document.querySelector('#sub-bg-color').value || '#000';
  const bgToggle = document.querySelector('#sub-bg-toggle').checked;
  const offsetY = document.querySelector('#sub-offsetY').value || 85;
  const delay = parseInt(document.querySelector('#sub-delay').value || 0);

const css = `
::cue {
  color: ${color} !important;
  ${bgToggle ? `background-color: ${hexToRgba(bg, 0.7)} !important;` : 'background: none !important;'}
  font-size: ${size}px !important;
  text-shadow: 1px 1px 2px black;
  line-height: 1.2;
}
video::cue {
  color: ${color} !important;
  ${bgToggle ? `background-color: ${hexToRgba(bg, 0.7)} !important;` : 'background: none !important;'}
}
track::cue {
  color: ${color} !important;
  ${bgToggle ? `background-color: ${hexToRgba(bg, 0.7)} !important;` : 'background: none !important;'}
}
`;
style.textContent = css;


  document.querySelectorAll('video').forEach(video => {
    const tracks = video.textTracks;
    for (let i = 0; i < tracks.length; i++) {
      const track = tracks[i];
      for (let j = 0; j < track.cues.length; j++) {
        const cue = track.cues[j];
        if (!cue.__originalStart) {
          cue.__originalStart = cue.startTime;
          cue.__originalEnd = cue.endTime;
        }
        cue.startTime = Math.max(0, cue.__originalStart + delay / 1000);
        cue.endTime = Math.max(0, cue.__originalEnd + delay / 1000);

        cue.snapToLines = false;
        cue.line = parseFloat(offsetY);
      }
      const wasMode = track.mode;
      track.mode = 'hidden';
      track.mode = 'showing';
    }
  });
}

         window.addEventListener('resize', applySettings);
     document.addEventListener('fullscreenchange', applySettings);

  function saveSettings() {
    const current = {
      fontSize: document.querySelector('#sub-font-size').value || 30,
      fontColor: document.querySelector('#sub-font-color').value || '#fff',
      bgColor: document.querySelector('#sub-bg-color').value || '#000',
      bgToggle: document.querySelector('#sub-bg-toggle').checked,
      offsetY: document.querySelector('#sub-offsetY').value || 85,
      delay: parseInt(document.querySelector('#sub-delay').value || 0)
    };
    localStorage.setItem('__subtitle_settings__', JSON.stringify(current));
  }

  function loadSettings() {
    const saved = localStorage.getItem('__subtitle_settings__');
    const s = saved ? { ...defaultSettings, ...JSON.parse(saved) } : { ...defaultSettings };
    s.delay = 0; // reset delay every reload
    return s;
  }

  function hexToRgba(hex, alpha) {
    const bigint = parseInt(hex.replace('#', ''), 16);
    const r = (bigint >> 16) & 255;
    const g = (bigint >> 8) & 255;
    const b = bigint & 255;
    return `rgba(${r}, ${g}, ${b}, ${alpha})`;
  }

  function attachSubtitle(video, vttURL) {
    video.querySelectorAll('track.__custom_subtitle__').forEach(t => t.remove());

    const track = document.createElement('track');
    track.label = 'Custom Subtitle';
    track.kind = 'subtitles';
    track.srclang = 'en';
    track.src = vttURL;
    track.default = true;
    track.classList.add('__custom_subtitle__');

    video.appendChild(track);


  }

})();