diff options
Diffstat (limited to 'youtube')
-rw-r--r-- | youtube/static/js/common.js | 32 | ||||
-rw-r--r-- | youtube/static/js/hotkeys.js | 19 | ||||
-rw-r--r-- | youtube/static/js/transcript-table.js | 133 | ||||
-rw-r--r-- | youtube/templates/watch.html | 30 |
4 files changed, 210 insertions, 4 deletions
diff --git a/youtube/static/js/common.js b/youtube/static/js/common.js new file mode 100644 index 0000000..4c7ee51 --- /dev/null +++ b/youtube/static/js/common.js @@ -0,0 +1,32 @@ +Q = document.querySelector.bind(document); +function text(msg) { return document.createTextNode(msg); } +function clearNode(node) { while (node.firstChild) node.removeChild(node.firstChild); } +function toMS(s) { + var s = Math.floor(s); + var m = Math.floor(s/60); var s = s % 60; + return `0${m}:`.slice(-3) + `0${s}`.slice(-2); +} + + +var cur_tt_idx = 0; +function getActiveTranscriptTrackIdx() { + let tts = Q("video").textTracks; + if (!tts.length) return; + for (let i=0; i < tts.length; i++) { + if (tts[i].mode == "showing") { + cur_tt_idx = i; + return cur_tt_idx; + } + } + return cur_tt_idx; +} +function getActiveTranscriptTrack() { return Q("video").textTracks[getActiveTranscriptTrackIdx()]; } + +function getDefaultTranscriptTrackIdx() { + let tts = Q("video").textTracks; + return tts.length - 1; +} + +window.addEventListener('DOMContentLoaded', function() { + cur_tt_idx = getDefaultTranscriptTrackIdx(); +}); diff --git a/youtube/static/js/hotkeys.js b/youtube/static/js/hotkeys.js index c696ac0..b295db9 100644 --- a/youtube/static/js/hotkeys.js +++ b/youtube/static/js/hotkeys.js @@ -1,12 +1,11 @@ -Q = document.querySelector.bind(document); - function onKeyDown(e) { if (['INPUT', 'TEXTAREA'].includes(document.activeElement.tagName)) return false; - console.log(e); + // console.log(e); let v = Q("video"); let c = e.key.toLowerCase(); - if (c == "k") { + if (e.ctrlKey) return; + else if (c == "k") { v.paused ? v.play() : v.pause(); } else if (c == "arrowleft") { @@ -25,6 +24,18 @@ function onKeyDown(e) { e.preventDefault(); v.currentTime = v.currentTime + 10; } + else if (c == "f") { + e.preventDefault(); + if (document.fullscreen) document.exitFullscreen(); + else v.requestFullscreen(); + } + else if (c == "c") { + e.preventDefault(); + let tt = getActiveTranscriptTrack(); + if (tt == null) return; + if (tt.mode == "showing") tt.mode = "disabled"; + else tt.mode = "showing"; + } } window.addEventListener('DOMContentLoaded', function() { diff --git a/youtube/static/js/transcript-table.js b/youtube/static/js/transcript-table.js new file mode 100644 index 0000000..26d2948 --- /dev/null +++ b/youtube/static/js/transcript-table.js @@ -0,0 +1,133 @@ +var details_tt, select_tt, table_tt; + +function renderCues() { + var tt = Q("video").textTracks[select_tt.selectedIndex]; + let cuesL = [...tt.cues]; + var tt_type = cuesL[0].text.startsWith(" \n"); + let ff_bug = false; + if (!cuesL[0].text.length) { ff_bug = true; tt_type = true }; + let rows; + + function forEachCue(cb) { + for (let i=0; i < cuesL.length; i++) { + let txt, startTime = tt.cues[i].startTime; + if (tt_type) { + if (i % 2) continue; + if (ff_bug && !tt.cues[i].text.length) txt = tt.cues[i+1].text; + else txt = tt.cues[i].text.split('\n')[1].replace(/<[\d:.]*?><c>(.*?)<\/c>/g, "$1"); + } else { + txt = tt.cues[i].text; + } + cb(startTime, txt); + } + } + + function createA(startTime, txt, title=null) { + a = document.createElement("a"); + a.appendChild(text(txt)); + a.href = "javascript:;"; // TODO: replace this with ?t parameter + if (title) a.title = title; + a.addEventListener("click", (e) => { + Q("video").currentTime = startTime; + }) + return a; + } + + clearNode(table_tt); + console.log("render cues..", tt.cues.length); + if (Q("input#transcript-use-table").checked) { + forEachCue((startTime, txt) => { + let tr, td, a; + tr = document.createElement("tr"); + + td = document.createElement("td") + td.appendChild(createA(startTime, toMS(startTime))); + tr.appendChild(td); + + td = document.createElement("td") + td.appendChild(text(txt)); + tr.appendChild(td); + + table_tt.appendChild(tr); + }); + rows = table_tt.rows; + } + else { + forEachCue((startTime, txt) => { + span = document.createElement("span"); + var idx = txt.indexOf(" ", 1); + var [firstWord, rest] = [txt.slice(0, idx), txt.slice(idx)]; + + span.appendChild(createA(startTime, firstWord, toMS(startTime))); + if (rest) span.appendChild(text(rest + " ")); + table_tt.appendChild(span); + }); + rows = table_tt.childNodes; + } + + var lastActiveRow = null; + function colorCurRow(e) { + // console.log("cuechange:", e); + var idxC = cuesL.findIndex((c) => c == tt.activeCues[0]); + var idxT = tt_type ? Math.floor(idxC / 2) : idxC; + + if (lastActiveRow) lastActiveRow.style.backgroundColor = ""; + if (idxT < 0) return; + var row = rows[idxT]; + row.style.backgroundColor = "#0cc12e42"; + lastActiveRow = row; + } + colorCurRow(); + tt.addEventListener("cuechange", colorCurRow); +} + +function loadCues() { + let tts = Q("video").textTracks; + let tt = tts[select_tt.selectedIndex]; + let dst_mode = "hidden"; + for (let ttI of tts) { + if (ttI.mode === "showing") dst_mode = "showing"; + if (ttI !== tt) ttI.mode = "disabled"; + } + if (tt.mode == "disabled") tt.mode = dst_mode; + + var iC = setInterval(() => { + if (tt.cues && tt.cues.length) { + clearInterval(iC); + renderCues(); + } + }, 100); +} + +window.addEventListener('DOMContentLoaded', function() { + let tts = Q("video").textTracks; + if (!tts.length) return; + + details_tt = Q("details#transcript-details"); + details_tt.addEventListener("toggle", () => { + if (details_tt.open) loadCues(); + }); + + select_tt = Q("select#select-tt"); + select_tt.selectedIndex = getDefaultTranscriptTrackIdx(); + select_tt.addEventListener("change", loadCues); + + table_tt = Q("table#transcript-table"); + table_tt.appendChild(text("loading..")); + + tts.addEventListener("change", (e) => { + // console.log(e); + var idx = getActiveTranscriptTrackIdx(); // sadly not provided by 'e' + if (tts[idx].mode == "showing") { + select_tt.selectedIndex = idx; + loadCues(); + } + else if (details_tt.open && tts[idx].mode == "disabled") { + tts[idx].mode = "hidden"; // so we still receive 'oncuechange' + } + }) + + Q("input#transcript-use-table").addEventListener("change", renderCues); + + Q(".side-videos").prepend(details_tt); +}); diff --git a/youtube/templates/watch.html b/youtube/templates/watch.html index 5ecf7ae..e3c6fa0 100644 --- a/youtube/templates/watch.html +++ b/youtube/templates/watch.html @@ -305,6 +305,18 @@ .format-codecs{ width: 120px; } + + table#transcript-table { + border-collapse: collapse; + width: 100%; + } + table#transcript-table td, th { + border: 1px solid #dddddd; + } + div#transcript-div { + background-color: var(--interface-color); + padding: 5px; + } {% endblock style %} {% block main %} @@ -582,6 +594,21 @@ Reload without invidious (for usage of new identity button).</a> </div> {% endif %} + {% if subtitle_sources %} + <details id="transcript-details"> + <summary>Transcript</summary> + <div id="transcript-div"> + <select id="select-tt"> + {% for source in subtitle_sources %} + <option>{{ source['label'] }}</option> + {% endfor %} + </select> + <input type="checkbox" id="transcript-use-table"> + <table id="transcript-table"></table> + </div> + </details> + {% endif %} + {% if settings.related_videos_mode != 0 %} <details class="related-videos-outer" {{'open' if settings.related_videos_mode == 1 else ''}}> <summary>Related Videos</summary> @@ -608,7 +635,10 @@ Reload without invidious (for usage of new identity button).</a> </details> {% endif %} {% endif %} + + <script src="/youtube.com/static/js/common.js"></script> {% if settings.use_video_hotkeys %} <script src="/youtube.com/static/js/hotkeys.js"></script> {% endif %} + <script src="/youtube.com/static/js/transcript-table.js"></script> {% endblock main %} |