Subtitle Uploader

Upload subtitles to any video on any website + settings panel

// ==UserScript==
// @name         Subtitle Uploader
// @namespace    http://tampermonkey.net/
// @version      3.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(225deg, rgba(204, 204, 204, 0.18) 0%, #ffffff 0.03%, rgba(46, 133, 121, 0.24) 0.88%, rgba(5, 18, 17, 0.38) 25.56%, rgba(63, 85, 79, 0.29) 37.25%, rgba(15, 26, 22, 0.53) 82.63%, rgba(0, 0, 0, 0.58) 98.5%), linear-gradient(135deg, rgba(204, 204, 204, 0.18) 0%, #ffffff 0.03%, rgba(46, 133, 121, 0.24) 0.88%, rgba(5, 18, 17, 0.38) 25.56%, rgba(63, 85, 79, 0.29) 37.25%, rgba(15, 26, 22, 0.53) 82.63%, rgba(0, 0, 0, 0.58) 98.5%), linear-gradient(270deg, rgba(204, 204, 204, 0.18) 0%, #ffffff 0.03%, rgba(46, 133, 121, 0.24) 0.88%, rgba(5, 18, 17, 0.38) 25.56%, rgba(63, 85, 79, 0.29) 37.25%, rgba(15, 26, 22, 0.53) 82.63%, rgba(0, 0, 0, 0.58) 98.5%);
    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: 0.5px solid rgba(5, 180, 166, 0.9);
    backdrop-filter: blur(6px);
}

.subtitle-settings-panel h3 {
    margin-top: 0;
    margin-bottom: 0px;
    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;
    color: #fff;
    border-radius: 8px;
    background: rgba(255, 255, 255, 0.15);
    border: 1px solid rgba(255, 255, 255, 0.25);
    backdrop-filter: blur(15px);
    transition: all 0.3s ease;
    box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}

.subtitle-settings-panel input:focus,
.subtitle-settings-panel select:focus {
    outline: none;
    border-color: rgba(255, 255, 255, 0.4);
    box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.1);
    background: rgba(255, 255, 255, 0.2);
    transform: scale(1.02);
}

.subtitle-settings-panel button {
    background: linear-gradient(118deg, rgba(48, 123, 123, 0) 4.09%, rgba(48, 93, 84, 0.7) 58.71%);
    border: 1px solid rgba(255, 255, 255, 0.2);
    transition: all 0.3s ease;
    backdrop-filter: blur(10px);
    box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
    color: #fff;
    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: linear-gradient(90deg, #22c55e, #3b82f6, #8b5cf6);
    background-size: 200% 100%;
}

input:checked+.slider:before {
    transform: translateX(20px);
}

input[type="range"] {
    -webkit-appearance: none;
    /* Chrome, Safari, Opera, Edge */
    -moz-appearance: none;
    /* Firefox */
    appearance: none;
    /* CSS Standard */
    background: transparent;
    /* إزالة الخلفية الافتراضية في بعض المتصفحات */
    width: 79%;
    /* لضمان أن العنصر يأخذ العرض الكامل */
}


/* استايل الكرة في متصفحات كروم وسفاري وإيدج */
input[type="range"]::-webkit-slider-thumb {
    -webkit-appearance: none;
    /* ضروري لإلغاء المظهر الافتراضي أولاً */
    width: 15px;
    height: 15px;
    /* ارتفاع الكرة */
    background: linear-gradient(90deg, #22c55e, #3b82f6, #8b5cf6);
    background-size: 200% 100%;
    border-radius: 50%;
    /* لجعلها دائرية الشكل */
    cursor: pointer;
}

/* استايل الكرة في متصفح فايرفوكس */
input[type="range"]::-moz-range-thumb {
    width: 15px;
    height: 15px;
    background: linear-gradient(90deg, #22c55e, #3b82f6, #8b5cf6);
    background-size: 200% 100%;
    border-radius: 50%;
    border: none;
    /* فايرفوكس قد يضيف حدودًا تلقائية */
    cursor: pointer;
}

/* استايل الكرة في متصفح انترنت إكسبلورر */
input[type="range"]::-ms-thumb {
    width: 15px;
    height: 15px;
    background: linear-gradient(90deg, #22c55e, #3b82f6, #8b5cf6);
    background-size: 200% 100%;
    border-radius: 50%;
    cursor: pointer;
}

/* إزالة الـ focus outline الافتراضي المزعج */
input[type="range"]:focus {
    outline: none;
}

#copy-crypto:hover {
    opacity: 0.8;
    /* اجعل الأيقونة شفافة قليلاً عند مرور الفأرة */
    transform: scale(1.1);
    /* كبّر الأيقونة قليلاً */
}

#copy-crypto:active {
    transform: scale(0.9);
    /* صغّر الأيقونة قليلاً عند الضغط */
}

</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>
<label>_________________________________________</label>

<div style="display:flex;gap:15px;margin-top:15px;align-items:center;">

    <div style="flex:1.5;">
        <label>Font Color</label>
        <input type="color" id="sub-font-color" value="${settings.fontColor}" style="width: 79%">
    </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: 10px; white-space: nowrap;">
        bg-opacity & Background Color
        <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}">
        </div>
        <div style="flex:0.35;">
            <span id="bg-opacity-value" style="font-size: 10px; color: #fff;">${(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="flex: 1; display: flex; flex-direction: row; align-items: center; gap: 8px;">
    <div style="flex:1.2;display:flex;flex-direction:Column;">
 <label>Position</label>
        <input type="range" min="0" max="100" id="sub-offsetY" value="${settings.offsetY}">
    </div>
    <div style="flex:0.35;">
        <span id="sub-offsetY-value" style="font-size:10px;color:#fff;">${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">
    <button id="copy-crypto" title="Copy"
        style="background:#303835;border:none;color:#fff;margin-bottom:10px;cursor:pointer"> copy </button>
</div>

<div style="display: flex; justify-content: space-between; margin-top: 8px;">
    <button id="sub-clear">🧹 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 });


})();