// ==UserScript== // @name Dropout Captions Editor // @version 1.1 // @author giandrop // @namespace https://github.com/giandrop // @license MIT // @description A userscript that let you import, edit and save captions from any VHX video. // @homepage https://github.com/giandrop/Dropout-userscripts/tree/master/Dropout%20Captions%20Editor // @include *embed.vhx.tv* // @grant none // ==/UserScript== //DROPOUT COLOR: rgba(23, 35, 34, 0.75) //Icon (from Bootstrap: https://icons.getbootstrap.com/icons/chat-square-dots-fill/) const ICON = ` Custom subtitles `; const editor_html = `
Dropout Captions Editor
Export
`; const FONT = '"Helvetica Neue",Helvetica,Arial!important'; const STYLE = ` .editor { color: white; background-color: rgba(23,35,34,.75); font-family: ${FONT}; border: 1px solid grey; border-radius:5px; height: 60%; width: 30%; position:absolute; top:10%; left:10px; display:none; flex-flow: column; resize: both; overflow: auto; padding: 5px; } .editor > div { padding: 4px; display: flex; justify-content: space-between; align-items: center; } #time_display { width: 100px; } #playback-rate { width: 50px; } .dropout-cc-button { background-color: grey; color: white; font-family: ${FONT}; font-size: 0.9em; border: none; text-decoration: none; text-align: center; cursor: default; padding: 2px 6px; } .dropout-cc-button:hover { background-color: lightgrey; } .editor textarea { color: white; background-color: transparent;/*rgba(23,35,34,.75);*/ } .editor textarea::selection { color: black; background-color: yellow; } `; function string_to_date(hh, mm, ss, mil) { let date = new Date(0) date.setUTCHours(hh) date.setUTCMinutes(mm) date.setUTCSeconds(ss) date.setUTCMilliseconds(mil) return date } (function() { 'use strict'; var subs = ''; //Wait until video is loaded function find_video() { const video = document.querySelector('video'); if (video) { clearInterval(poll) get_subtitles() .then(res => res.text()) .then(text => {subs = text}) .finally(add_button) } } const poll = setInterval(find_video, 500) function get_subtitles() { let video = document.querySelector('video') let track = video.querySelector('track') if (track) { return fetch(track.src) } else { return Promise.reject('No tracks!') } } //As soon as the video is found this function is called function add_button(){ //Install stylesheet const stylesheet = document.createElement('style') stylesheet.innerHTML = STYLE document.body.appendChild(stylesheet) //Insert window in html const editor = document.createElement('div') editor.classList.add('editor') editor.innerHTML = editor_html const textarea = editor.querySelector('textarea') textarea.value = subs //Video const video = document.querySelector('video') const time_display = editor.querySelector('#time_display') // Handlers function save_handler() { //save textarea value to subs subs = textarea.value //convert subs to base64 let encoded = btoa(unescape(encodeURIComponent(subs))) //add new track let track = video.querySelector('track')//document.createElement('track') //If it doesn't exist we create one if (!track) { track = document.createElement('track') video.appendChild(track) } track.src = `data:text/vtt;charset=UTF-8;base64,${encoded}` track.default = true } function cancel_handler() { //reset textarea textarea.value = subs //reset playback rate playback_rate.value = '' //hide editor editor.style.display = 'none' } function import_handler(evt) { const file = evt.target.files[0] const reader = new FileReader(); reader.addEventListener('loadend', function(e){ subs = e.target.result textarea.value = subs evt.target.value = '' }); reader.readAsText(file); } function export_handler(evt) { //convert subs to base64 let encoded = btoa(unescape(encodeURIComponent(textarea.value))) //set URL to href evt.target.setAttribute('href', `data:text/vtt;charset=UTF-8;base64,${encoded}`); } function time_update_handler(evt) { let d = new Date(Date.UTC(0)) d.setUTCMilliseconds(video.currentTime * 1000) let hh = String(d.getUTCHours()).padStart(2,'0') let mm = String(d.getUTCMinutes()).padStart(2,'0') let ss = String(d.getUTCSeconds()).padStart(2,'0') let mil = String(d.getUTCMilliseconds()).padStart(3,'0') let time = `${hh}:${mm}:${ss}.${mil}` time_display.value = time } const save_btn = editor.querySelector('#save_btn') save_btn.addEventListener('click', save_handler) const cancel_btn = editor.querySelector('#cancel_btn') cancel_btn.addEventListener('click', cancel_handler) const vtt_input = editor.querySelector('#import-vtt') vtt_input.addEventListener('change', import_handler) const export_btn = editor.querySelector('#export-vtt') export_btn.addEventListener('click', export_handler) const set_playback_rate_btn = editor.querySelector('#set-playback-rate-btn') const playback_rate = editor.querySelector('#playback-rate') set_playback_rate_btn.addEventListener('click', () => {video.playbackRate = playback_rate.value}) video.addEventListener('timeupdate', time_update_handler) //Playbar button let button = document.createElement('button') button.innerHTML = ICON button.style = "margin-left: 10px; margin-top: -1px; color: white" button.addEventListener('click', () => { editor.style.display = 'flex' playback_rate.value = video.playbackRate }) document.body.appendChild(editor) //We insert it after the CC button let cc_button = document.querySelector('.toggle.cc') if (cc_button) { cc_button.insertAdjacentElement('afterend', button) } else { document.querySelector('.volume').insertAdjacentElement('afterend', button) } } })();