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