您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Upload subtitles to any video on any website + settings panel
// ==UserScript== // @name Subtitle Uploader // @namespace http://tampermonkey.net/ // @version 2.0 // @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, bgOpacity: 0.7 }; 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(); applySettings(); positionButtons(); setInterval(() => { positionButtons(); }, 2500); window.addEventListener('resize', positionButtons); document.addEventListener('fullscreenchange', positionButtons); function positionButtons() { document.querySelectorAll('.subtitle-controls').forEach(e => e.remove()); if (document.fullscreenElement) return; document.querySelectorAll('video').forEach(video => { const container = document.createElement('div'); container.className = 'subtitle-controls'; const btnUpload = document.createElement('button'); btnUpload.title = 'Upload Subtitle'; btnUpload.style.cssText = btnStyle(); btnUpload.innerHTML = ` <svg xmlns="http://www.w3.org/2000/svg" viewBox="8 8 20 20" width="20" height="20" fill="currentColor"> <path d="M11,11 C9.9,11 9,11.9 9,13 L9,23 C9,24.1 9.9,25 11,25 L25,25 C26.1,25 27,24.1 27,23 L27,13 C27,11.9 26.1,11 25,11 L11,11 Z M11,17 L14,17 L14,19 L11,19 L11,17 L11,17 Z M20,23 L11,23 L11,21 L20,21 L20,23 L20,23 Z M25,23 L22,23 L22,21 L25,21 L25,23 L25,23 Z M25,19 L16,19 L16,17 L25,17 L25,19 L25,19 Z" fill="#fff"></path> </svg> `; const btnSettings = document.createElement('button'); btnSettings.title = 'Settings'; btnSettings.style.cssText = btnStyle(); btnSettings.innerHTML = ` <svg xmlns="http://www.w3.org/2000/svg" viewBox="8 8 20 20" width="20" height="20" fill="currentColor"> <path d="m 23.94,18.78 c .03,-0.25 .05,-0.51 .05,-0.78 0,-0.27 -0.02,-0.52 -0.05,-0.78 l 1.68,-1.32 c .15,-0.12 .19,-0.33 .09,-0.51 l -1.6,-2.76 c -0.09,-0.17 -0.31,-0.24 -0.48,-0.17 l -1.99,.8 c -0.41,-0.32 -0.86,-0.58 -1.35,-0.78 l -0.30,-2.12 c -0.02,-0.19 -0.19,-0.33 -0.39,-0.33 l -3.2,0 c -0.2,0 -0.36,.14 -0.39,.33 l -0.30,2.12 c -0.48,.2 -0.93,.47 -1.35,.78 l -1.99,-0.8 c -0.18,-0.07 -0.39,0 -0.48,.17 l -1.6,2.76 c -0.10,.17 -0.05,.39 .09,.51 l 1.68,1.32 c -0.03,.25 -0.05,.52 -0.05,.78 0,.26 .02,.52 .05,.78 l -1.68,1.32 c -0.15,.12 -0.19,.33 -0.09,.51 l 1.6,2.76 c .09,.17 .31,.24 .48,.17 l 1.99,-0.8 c .41,.32 .86,.58 1.35,.78 l .30,2.12 c .02,.19 .19,.33 .39,.33 l 3.2,0 c .2,0 .36,-0.14 .39,-0.33 l .30,-2.12 c .48,-0.2 .93,-0.47 1.35,-0.78 l 1.99,.8 c .18,.07 .39,0 .48,-0.17 l 1.6,-2.76 c .09,-0.17 .05,-0.39 -0.09,-0.51 l -1.68,-1.32 0,0 z m -5.94,2.01 c -1.54,0 -2.8,-1.25 -2.8,-2.8 0,-1.54 1.25,-2.8 2.8,-2.8 1.54,0 2.8,1.25 2.8,2.8 0,1.54 -1.25,2.8 -2.8,2.8 l 0,0 z" fill="#fff"></path> </svg> `; 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); applySettings(); }; 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 - 80}px; z-index: 99999; display: flex; overflow: hidden; border-radius: 30%; gap:2px; `; }); } 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); track.addEventListener('load', () => { applySettings(); }); setTimeout(() => { const cuesVisible = Array.from(video.textTracks) .some(t => t.cues && t.cues.length > 0); if (!cuesVisible) { if (!document.querySelector('.manual-subtitle')) { const div = document.createElement('div'); div.className = 'manual-subtitle'; div.style.cssText = ` position: absolute; bottom: 10%; width: 100%; text-align: center; color: white; font-size: 20px; text-shadow: 1px 1px 2px black; z-index: 100000; `; div.textContent = 'Subtitle loaded (manual fallback)'; video.parentElement.appendChild(div); } } }, 1000); } function btnStyle() { return ` background: rgba(0, 0, 0, 0.50); color: #fff; border: none; width: 32px; border-radius: 20px; height: 32px; font-size: 16.5px; cursor: pointer; display: flex; align-items: center; justify-content: center; padding: 0; `; } function createSettingsPanel() { const panel = document.createElement('div'); panel.className = 'subtitle-settings-panel'; panel.innerHTML = ` <style> .subtitle-settings-panel { position: fixed; top: 60px; right: 30px; background: linear-gradient(100deg, #000000ff, #03021bff); color: #fff; padding: 20px; border-radius: 20px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6); z-index: 100000; display: none; font-size: 14px; width: 280px; font-family: Arial, sans-serif; max-height: 80vh; overflow-y: auto; border: 1px solid rgba(216, 14, 14, 0.9); backdrop-filter: blur(6px); } .subtitle-settings-panel h3 { margin-top: 0; margin-bottom: 15px; font-size: 18px; color: #ddd; text-align: center; } .subtitle-settings-panel label { display: block; margin: 6px 0 2px; font-weight: bold; font-size: 12px; } .subtitle-settings-panel input, .subtitle-settings-panel select { width: 100%; margin-bottom: 10px; padding: 8px; border: none; border-radius: 8px; background: #181616ff; color: #fff; outline: 2px solid transparent; transition: all 0.2s ease; } .subtitle-settings-panel input:focus, .subtitle-settings-panel select:focus { background: #160101ff; } .subtitle-settings-panel button { background: #f80000ff; color: #fff; border: none; padding: 6px 12px; border-radius: 8px; cursor: pointer; transition: background 0.2s ease; } .subtitle-settings-panel button:hover { background: #571313fb; } .subtitle-settings-panel .validation-text { color: #10b981; font-size: 16px; margin-top: 4px; } .switch { position: relative; display: inline-block; width: 40px; height: 20px; } .switch input { opacity: 0; width: 0; height: 0; } .slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: .4s; border-radius: 20px; } .slider:before { position: absolute; content: ""; height: 16px; width: 16px; left: 2px; bottom: 2px; background-color: white; transition: .4s; border-radius: 50%; } input:checked + .slider { background-color: #cf0c0cff; } input:checked + .slider:before { transform: translateX(20px); } </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.5;"> <label>Font Color</label> <input type="color" id="sub-font-color" value="${settings.fontColor}"> </div> <div style="flex:1;"> <label>Font Size</label> <input type="number" id="sub-font-size" value="${settings.fontSize}" style="width:81%"> </div> </div> <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px; flex-wrap: wrap;"> <label style="display: flex; align-items: center; gap: 50px; white-space: nowrap;"> Background Color & bg-opacity <label class="switch"> <input type="checkbox" id="sub-bg-toggle" checked> <span class="slider"></span> </label> </label> <div style="flex: 1; display: flex; flex-direction: row; align-items: center; gap: 8px;"> <div style="flex:1.2;display:flex;flex-direction:Column;align-items:center;"> <input type="range" min="0" max="1" step="0.01" id="sub-bg-opacity" value="${settings.bgOpacity || 0.7}" style="width:79%;accent-color:#f44336;cursor:pointer;"> </div> <div style="flex:0.35;"> <span id="bg-opacity-value" style="font-size: 10px; color: #f44336;">${(settings.bgOpacity || 0.7) * 100}%</span> </div> <div style="flex: 1; display: flex; flex-direction: column;"> <input type="color" id="sub-bg-color" value="${settings.bgColor}" style="width:94%"> </div> </div> </div> <div style="display:flex;gap:4px;align-items:center;"> <div style="flex:1.2;display:flex;flex-direction:Column;align-items:center;"> <label>Position</label> <input type="range" min="0" max="100" id="sub-offsetY" value="${settings.offsetY}" style="width:79%;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="50" style="width:81%"> </div> </div> <h3>💰 Donate Me❤️</h3> <select id="crypto-select"> <option value="">-- Choose --</option> ${Object.entries(cryptoAddresses).map(([k,v]) => `<option value="${v}">${k}</option>`).join('')} </select> <div style="display:flex;gap:4px;margin-top:4px;align-items:center;"> <input type="text" id="crypto-output" readonly placeholder="Selected address" style="background:#333;color:#fff;border:none;padding:2px;"> <button id="copy-crypto" title="Copy" style="background:none;border:none;color:#fff;font-size:16px;width:5%;cursor:pointer">⎘</button> </div> <div style="display: flex; justify-content: space-between; margin-top: 8px;"> <button id="sub-clear" style="background: #444;">🧹 Clear Subtitle</button> <button id="sub-close">Close</button> </div> `; document.body.appendChild(panel); panel.querySelector('#sub-offsetY').addEventListener('input', () => { panel.querySelector('#sub-offsetY-value').textContent = panel.querySelector('#sub-offsetY').value + 'px'; }); panel.querySelector('#sub-close').onclick = () => panel.style.display = 'none'; panel.querySelector('#sub-clear').onclick = () => { document.querySelectorAll('video').forEach(video => { // إزالة كل التراكات video.querySelectorAll('track').forEach(t => t.remove()); // إزالة fallback يدوي لو موجود video.closest('body')?.querySelector('.manual-subtitle')?.remove(); // تعطيل الـ textTracks Array.from(video.textTracks).forEach(track => { track.mode = 'disabled'; }); }); // إزالة الستايل تبع ::cue if (style && style.parentNode) { style.remove(); } alert('✅ Translation removed, page will be reloaded for full cleanup.'); setTimeout(() => { location.reload(); }, 600); // ندي وقت لل alert يظهر }; panel.querySelectorAll('input').forEach(input => { input.addEventListener('input', () => { saveSettings(); applySettings(); }); }); panel.querySelector('#sub-bg-opacity').addEventListener('input', () => { const val = panel.querySelector('#sub-bg-opacity').value; panel.querySelector('#bg-opacity-value').textContent = ` ${Math.round(val * 100)}%`; saveSettings(); applySettings(); }); panel.querySelector('#crypto-select').onchange = function () { panel.querySelector('#crypto-output').value = this.value; }; panel.querySelector('#copy-crypto').onclick = async () => { const out = panel.querySelector('#crypto-output').value; if (!out) return alert('Please select an address first!🫵🤨'); try { await navigator.clipboard.writeText(out); alert('Address copied!❤️'); } catch (err) { // Fallback for Chrome if permissions or context fail const temp = document.createElement('textarea'); temp.value = out; temp.style.position = 'fixed'; temp.style.left = '-9999px'; document.body.appendChild(temp); temp.select(); try { const success = document.execCommand('copy'); if (success) { alert('Address copied!❤️'); } else { throw new Error('execCommand failed'); } } catch (e) { alert('❌ Failed to copy. Please copy manually.'); } document.body.removeChild(temp); } }; 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 = parseFloat(document.querySelector('#sub-offsetY').value || 85); const delay = parseInt(document.querySelector('#sub-delay').value || 0); const opacity = parseFloat(document.querySelector('#sub-bg-opacity').value || 0.7); const css = ` ::cue { color: ${color} !important; ${bgToggle ? `background-color: ${hexToRgba(bg, opacity)} !important;` : 'background: none !important;'} font-size: ${size}px !important; text-shadow: 1px 1px 2px black; line-height: 1.2; }`; style.textContent = css; // تنظيف أي manual fallback قديم document.querySelector('.manual-subtitle')?.remove(); 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); } // Force refresh subtitles track.mode = 'disabled'; // reset completely setTimeout(() => { track.mode = 'showing'; // re-enable to apply timing }, 10); } // أيضاً نحاول إخفاء أي fallback manual text video.parentElement?.querySelector('.manual-subtitle')?.remove(); }); } 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), bgOpacity: parseFloat(document.querySelector('#sub-bg-opacity').value || 0.7), }; localStorage.setItem('__subtitle_settings__', JSON.stringify(current)); } function loadSettings() { const saved = localStorage.getItem('__subtitle_settings__'); return saved ? { ...defaultSettings, ...JSON.parse(saved) } : { ...defaultSettings }; } 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})`; } let lastUrl = location.href; new MutationObserver(() => { const url = location.href; if (url !== lastUrl) { lastUrl = url; setTimeout(() => { positionButtons(); }, 1500); // استنى شوية لحد ما الفيديو يظهر } }).observe(document, { subtree: true, childList: true }); })();