Plays back threads on 4chan X
- // ==UserScript==
- // @name 4chan X Thread Playback
- // @namespace VSJPlus
- // @license GNU GPLv3
- // @description Plays back threads on 4chan X
- // @version 1.0.3
- // @match *://boards.4chan.org/*/*/*
- // @match *://boards.4channel.org/*/*/*
- // @run-at document-start
- // @require https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js
- // @require https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js
- // @require https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js
- // @grant GM.info
- // @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAZdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuMjHxIGmVAAAAkUlEQVQ4T6WQAQ6AMAgD9yf/5Gv906QYSBGGRk1OXFuaxbEf2y/0Necc8uCl4FxRZa4WEWQ6HPDgIhNuUAUePRFSwIDeeV6gH02YsRx2QoELTQkvel6MIKhYlFTL4NUNoDEhzwUw78vAdJ6poFqGZtiZvbbAAuxVmVTAsyswP/0DngbOXMB+KFiBBSZ4KPjONk6FWw9BoS3PSwAAAABJRU5ErkJggg==
- // ==/UserScript==
- console.log('4chan X Playback');
- (async function() {
- let isChanX;
- let fourChanXInitFinished = new Promise(res => {
- document.addEventListener(
- "4chanXInitFinished",
- function (event) {
- if (
- document.documentElement.classList.contains("fourchan-x") &&
- document.documentElement.classList.contains("sw-yotsuba")
- ) {
- isChanX = true;
- res();
- }
- }
- );
- });
- async function appendStyle() {
- var head = document.head;
- if(!head) {
- head = await new Promise(res => {
- let obs = new MutationObserver(mutations => {
- for(let mutation of mutations) {
- if(!mutation.addedNodes || !mutation.addedNodes.length)
- continue;
- for(let node of mutation.addedNodes) {
- if(node.matches('head')) {
- obs.disconnect();
- res(node);
- }
- }
- }
- });
- obs.observe(document.documentElement, {childList: true});
- });
- }
- let link = document.createElement('link');
- link.rel = 'stylesheet';
- link.style = 'text/css';
- link.href = 'https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.css';
- head.appendChild(link);
- var style = document.createElement('style');
- style.type = 'text/css';
- style.id = 'playbackStyle';
- style.innerHTML = `
- #playbackUI {
- position: absolute;
- top: 100%;
- margin: 0 auto;
- width: 90%;
- max-width: 1080px;
- box-sizing: border-box;
- background-color: #282a2e;
- opacity: 0.9;
- transition: opacity 100ms;
- display: none;
- z-index: 1;
- box-shadow: 0 0 10px #0008;
- padding: 10px;
- left: 0;
- right: 0;
- }
- #playbackSlider {
- background: #bbb;
- border-color: #999;
- cursor: pointer;
- position: relative;
- }
- #playbackSlider:hover {
- background: #c4c4c4;
- }
- #playbackSlider #timestampPreview {
- position: absolute;
- width: 0;
- height: 0;
- }
- #playbackSlider #timestampPreview:after,
- #playbackSlider .ui-slider-handle.ui-state-active:after {
- top: unset !important;
- bottom: 100%;
- margin-top: unset !important;
- margin-bottom: 5px;
- opacity: 0;
- transition: opacity 100ms;
- }
- #playbackSlider .ui-slider-handle.ui-state-active:after {
- margin-bottom: 2px;
- }
- #playbackSlider:hover #timestampPreview:after,
- #playbackSlider .ui-slider-handle.ui-state-active:after {
- animation: none !important;
- opacity: 1;
- }
- #playbackSlider .ui-slider-handle:not(.ui-state-active):after,
- #playbackSlider.ui-state-active #timestampPreview {
- display: none !important;
- }
- #playbackUI:hover, #playbackUI:focus {
- opacity: 1;
- }
- .playbackEnabled #playbackUI {
- display: flex;
- flex-direction: row;
- align-items: center;
- }
- .playbackEnabled #playbackUI #playbackTimeContainer {
- flex-shrink: 1;
- }
- .playbackEnabled #playbackUI #playbackSlider {
- flex-grow: 1;
- margin: 10px
- }
- #playbackInputContainer input {
- width: 15px;
- padding: 0;
- margin: 0;
- border: 0;
- font-size: 12px;
- font-weight: bold;
- }
- input#playbackInputYear {
- width: 30px;
- }
- #playbackTimeContainer {
- font-weight: bold;
- }
- #playbackDisplay > * > * {
- cursor: pointer;
- }
- #playbackDisplay > * > *:hover {
- color: #fff;
- }
- #playbackPauseResume {
- width: 20px;
- height: 27px;
- text-align: center;
- display: inline-block;
- cursor: pointer;
- position: relative;
- color: #ccc;
- transition: color 100ms;
- vertical-align: center;
- }
- #playbackPauseResume:hover {
- color: #fff;
- }
- #playbackPauseResume:before {
- font-size: 20px;
- display: block;
- position: absolute;
- user-select: none;
- text-align: center;
- top: -1.5px;
- }
- #playbackPauseResume:not(.pause):before {
- content: 'II';
- left: 2.5px;
- font-weight: 900;
- }
- #playbackPauseResume.pause:before {
- content: '';
- width: 13px;
- height: 15px;
- background-color: #ccc;
- left: 5px;
- top: 6px;
- clip-path: polygon(0 0, 13px 50%, 0 100%);
- }
- #playbackSkipBack,
- #playbackSkipAhead {
- cursor: pointer;
- position: relative;
- top: -0.5px;
- width: 18px;
- height: 20px;
- transition: transform 100ms;
- transform-origin: 10px 12px;
- }
- @keyframes skipBack {
- 0% { transform: rotate(0deg); }
- 25% { transform: rotate(-35deg); }
- 50% { transform: rotate(-20deg); }
- 100% { transform: rotate(-25deg); }
- }
- #playbackSkipBack:active {
- transform: rotate(-25deg);
- animation: skipBack 50ms;
- }
- @keyframes skipAhead {
- 0% { transform: rotate(0deg); }
- 25% { transform: rotate(35deg); }
- 50% { transform: rotate(20deg); }
- 100% { transform: rotate(25deg); }
- }
- #playbackSkipAhead:active {
- transform: rotate(25deg);
- animation: skipAhead 50ms;
- }
- #playbackSkipBackContainer {
- margin-left: 5px;
- }
- #playbackSkipBack .skipPath,
- #playbackSkipAhead .skipPath {
- fill: #ccc;
- transition: fill 100ms;
- }
- #playbackSkipBack:hover .skipPath,
- #playbackSkipAhead:hover .skipPath {
- fill: #fff;
- }
- #playbackSpeedDisplay {
- margin-left: 2px;
- font-weight: bold;
- cursor: pointer;
- }
- #playbackSpeedDisplay:hover {
- color: #fff;
- }
- #playbackSpeedInput {
- width: 35px;
- padding: 0;
- margin: 0;
- border: 0;
- text-align: right;
- font-weight: bold;
- font-size: 12px;
- }
- #playbackSpeedInput:after {
- content: 'x';
- }
- .playbackEnabled .playbackHidden {
- display: none !important;
- }
- .playbackEnabled .backlink.playbackHidden + .hashlink {
- display: none !important;
- }
- #playbackUI .ui-slider-handle {
- border-radius: 10px;
- background: #ccc !important;
- cursor: pointer;
- }
- #playbackUI .ui-slider-handle.ui-state-hover,
- #playbackUI .ui-slider-handle.ui-state-active {
- background: #fff !important;
- }
- #playbackDisplay > * > * {
- display: inline-block;
- }
- @media only screen and (max-width: 316px) {
- #playbackUI {
- width: 100%;
- margin: 0;
- }
- }
- .adc-resp-bg {
- display: none;
- }
- [tooltip] {
- position: relative;
- }
- [tooltip]:after {
- opacity: 0;
- content: attr(tooltip);
- position: absolute;
- background: #000;
- padding: 3px;
- top: 100%;
- margin-top: 10px;
- left: 50%;
- transform: translateX(-50%);
- border: 0.5px solid #ccc;
- border-radius: 3px;
- font-family: arial, helvetica, sans-serif;
- font-weight: normal;
- font-size: 10px;
- text-align: center;
- z-index: 1;
- pointer-events: none;
- color: #ccc;
- }
- [tooltip]:hover:after {
- animation: tooltipFade 800ms;
- opacity: 1;
- }
- @keyframes tooltipFade {
- 0% { opacity: 0; }
- 80% { opacity: 0; }
- 100% { opacity: 1; }
- }
- #playbackPauseResume.pause:hover:after {
- content: 'Play';
- }
- #playbackToggle.loading {
- opacity: 0.4;
- cursor: wait;
- }
- `;
- head.appendChild(style);
- }
- let slider, scrubbing = false, seeking = false, playing = true, startUnix, currentUnix, maxUnix;
- const delay = ms => new Promise(r => setTimeout(r, ms));
- const isArchived = () => document.querySelector('#update-status').innerText == 'Archived';
- const $q = s => document.querySelector(s);
- const $qa = s => document.querySelectorAll(s);
- const $id = id => document.getElementById(id);
- const aF = () => new Promise(r => window.requestAnimationFrame(r));
- function updatePlaybackSub(interval) {
- maxUnix = isArchived() ? parseInt(Object.values(posts).map(p => p.timestamp).sort((a,b) => b-a)[0]):moment().unix();
- let increment = 1000/playbackSpeed;
- if(playing) currentUnix += interval/increment;
- currentUnix = Math.min(currentUnix, maxUnix);
- slider.slider('option', 'max', maxUnix);
- slider.slider('option', 'value', currentUnix);
- }
- let lastUpdate, playbackSpeed = 1, correction = 0;
- const getIntervals = () => {
- let now = Date.now(), increment = 1000/playbackSpeed;
- if(!lastUpdate) lastUpdate = now - increment;
- let realInterval = now - lastUpdate;
- correction = increment - (now - lastUpdate - correction);
- lastUpdate = now;
- return [realInterval, increment + correction];
- }
- async function updatePlayback() {
- await delay(1000 - Date.now()%1000);
- while(true) {
- let [realInterval, adjustedInterval] = getIntervals();
- if(!scrubbing) updatePlaybackSub(realInterval);
- await delay(adjustedInterval);
- }
- }
- function splitArray(array, limit) {
- let arrays = [];
- for(let i = 0; i < array.length; i += limit) {
- arrays.push(array.slice(i, i + limit));
- }
- return arrays;
- }
- const playbackHiddenPosts = [document.createElement('style')];
- let lastHiddenPosts;
- async function updatePostVisibility() {
- let selectors = Object.values(posts).filter(p => p.timestamp > currentUnix).map(p => p.selectors),
- newPosts = lastHiddenPosts != selectors.length;
- if(!newPosts) return;
- lastHiddenPosts = selectors.length;
- let scrollToBottom = false, docEl = document.documentElement;
- if(newPosts && autoScroll && (docEl.offsetHeight - (docEl.scrollTop + window.innerHeight)) < 100) {
- scrollToBottom = true;
- }
- let css = splitArray(selectors, 500)
- .map(s => s.join(',')+'{display:none !important;}');
- while(playbackHiddenPosts.length < css.length) {
- let style = document.createElement('style');
- style.id = 'playbackHiddenPosts-'+playbackHiddenPosts.length;
- document.head.appendChild(style);
- playbackHiddenPosts.push(style);
- }
- for(let [k,v] of Object.entries(playbackHiddenPosts)) {
- playbackHiddenPosts[k].innerHTML = css[k] || '';
- }
- if(scrollToBottom) {
- await aF();
- docEl.scrollTop = docEl.offsetHeight - window.innerHeight;
- }
- }
- function updateDateTimeDisplay(unix) {
- let m = moment.unix(unix);
- [...$qa('#playbackDisplay [data-unit]')].forEach(e => (e.innerHTML = m.format(e.dataset.unit)));
- }
- const posts = {}, nextInput = {
- playbackInputYear: 'playbackInputMonth',
- playbackInputMonth: 'playbackInputDay',
- playbackInputDay: 'playbackInputHours',
- playbackInputHours: 'playbackInputMinutes',
- playbackInputMinutes: 'playbackInputSeconds'
- }
- function getPostData(id) {
- if(!id) return null;
- let selectors = [
- `.postContainer[data-full-i-d="${id}"]`,
- `.backlink[href="#p${id.split('.')[1]}"]`
- ];
- let postContainer = $q(selectors[0]);
- selectors.push(`${selectors[1]} + .hashlink`)
- return {
- selectors: selectors.map(s => 'html.playbackEnabled '+s).join(', '),
- timestamp: parseInt(postContainer.querySelector('.dateTime').dataset.utc)
- }
- }
- let autoScroll = false;
- function togglePlay(newPlaying) {
- if(newPlaying === undefined)
- newPlaying = !playing;
- playing = newPlaying;
- if(playing) $('#playbackPauseResume').removeClass('pause');
- else $('#playbackPauseResume').addClass('pause');
- }
- async function setupPlaybackToggle() {
- let threadingControl = document.querySelector('#threadingControl');
- if(!threadingControl) return;
- let autoScrollCheckbox = $q('input[name="Auto Scroll"]');
- autoScroll = autoScrollCheckbox.checked;
- $(autoScrollCheckbox).change(e => (autoScroll = autoScrollCheckbox.checked));
- threadingControl.parentNode
- .insertAdjacentHTML('afterend', '<label id="playbackToggle" class="entry"><input id="playbackToggleCheckbox" type="checkbox"> Playback</label>');
- let checkbox = document.querySelector('#playbackToggleCheckbox');
- checkbox.checked = document.documentElement.matches('.playbackEnabled');
- let toggle = $('#playbackToggle').hover(e => $(e.target).addClass('focused').siblings().removeClass('focused'));
- if(!isChanX) {
- toggle.addClass('loading');
- checkbox.setAttribute('disabled', '');
- await fourChanXInitFinished;
- }
- $(checkbox).click(() => {
- let checked = document.querySelector('#playbackToggleCheckbox').checked;
- $(document.documentElement).toggleClass('playbackEnabled');
- togglePlay(checked);
- });
- toggle.removeClass('loading');
- checkbox.removeAttribute('disabled');
- }
- function setupPlaybackUI() {
- $q('#header-bar').insertAdjacentHTML('beforeend', `
- <div id="playbackUI">
- <div id="playbackTimeContainer">
- <div id="playbackInputContainer" class="playbackHidden">
- <div id="playbackInputDate">
- <input type="text" id="playbackInputYear" maxlength="4" data-unit="yyyy" tooltip="Change Year">/<input type="text" id="playbackInputMonth" maxlength="2" data-unit="MM" tooltip="Change Month">/<input type="text" id="playbackInputDay" maxlength="2" data-unit="DD" tooltip="Change Day">
- </div>
- <div id="playbackInputTime">
- <input type="text" id="playbackInputHours" maxlength="2" data-unit="HH" tooltip="Change Hour">:<input type="text" id="playbackInputMinutes" maxlength="2" data-unit="mm" tooltip="Change Minutes">:<input type="text" id="playbackInputSeconds" maxlength="2" data-unit="ss" tooltip="Change Seconds">
- </div>
- </div>
- <div id="playbackDisplay">
- <div id="playbackDisplayDate">
- <div id="playbackDisplayYear" data-unit="yyyy" tooltip="Change Year">----</div>/<div id="playbackDisplayMonth" data-unit="MM" tooltip="Change Month">--</div>/<div id="playbackDisplayDay" data-unit="DD" tooltip="Change Day">--</div>
- </div>
- <div id="playbackDisplayTime">
- <div id="playbackDisplayHours" data-unit="HH" tooltip="Change Hour">----</div>:<div id="playbackDisplayMinutes" data-unit="mm" tooltip="Change Minutes">--</div>:<div id="playbackDisplaySeconds" data-unit="ss" tooltip="Change Seconds">--</div>
- </div>
- </div>
- </div>
- <div id="playbackSlider"></div>
- <input id="playbackSpeedInput" class="playbackHidden" type="text" tooltip="Playback Speed">
- <div id="playbackSpeedDisplay" type="text" tooltip="Playback Speed">1x</div>
- <div id="playbackSkipBackContainer" tooltip="Back 5s"><svg id="playbackSkipBack" tooltip="Back 5s" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="19.090909090909093" height="24.204545454545457"><path class="skipPath" d="M9.55 7.23L9.55 7.23L9.55 4.83Q11.06 4.83 12.38 5.39Q13.69 5.96 14.69 6.96Q15.69 7.96 16.25 9.28Q16.82 10.60 16.82 12.10L16.82 12.10Q16.82 13.61 16.26 14.93Q15.70 16.24 14.69 17.24Q13.69 18.25 12.38 18.81Q11.06 19.38 9.55 19.38L9.55 19.38Q8.04 19.38 6.72 18.81Q5.40 18.25 4.40 17.24Q3.40 16.24 2.84 14.93Q2.27 13.61 2.27 12.10L2.27 12.10L4.67 12.11Q4.67 13.12 5.05 14.00Q5.42 14.88 6.09 15.55Q6.77 16.22 7.65 16.60Q8.54 16.97 9.55 16.97L9.55 16.97Q10.55 16.97 11.44 16.60Q12.33 16.22 13.00 15.55Q13.67 14.88 14.04 14.00Q14.42 13.12 14.42 12.11L14.42 12.11Q14.42 11.09 14.04 10.21Q13.66 9.33 12.99 8.65Q12.32 7.98 11.44 7.61Q10.55 7.23 9.55 7.23ZM9.87 1.32L9.87 10.95L4.96 6.14L9.87 1.32Z"/></svg></div>
- <div id="playbackPauseResume" tooltip="Pause"></div>
- <div id="playbackSkipAheadContainer" tooltip="Skip 5s"><svg id="playbackSkipAhead" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="19.090909090909093" height="24.204545454545457"><path class="skipPath" d="M9.55 19.38L9.55 19.38Q8.03 19.38 6.72 18.81Q5.40 18.25 4.40 17.24Q3.39 16.24 2.83 14.93Q2.27 13.61 2.27 12.10L2.27 12.10Q2.27 10.60 2.84 9.28Q3.40 7.96 4.40 6.96Q5.40 5.96 6.72 5.39Q8.03 4.83 9.55 4.83L9.55 4.83L9.55 7.23Q8.54 7.23 7.65 7.61Q6.77 7.98 6.10 8.65Q5.43 9.33 5.05 10.21Q4.67 11.09 4.67 12.10L4.67 12.10Q4.67 13.12 5.05 14.00Q5.42 14.88 6.09 15.55Q6.76 16.22 7.65 16.60Q8.53 16.97 9.55 16.97L9.55 16.97Q10.55 16.97 11.44 16.60Q12.32 16.22 12.99 15.55Q13.66 14.88 14.04 14.00Q14.42 13.11 14.42 12.10L14.42 12.10L16.82 12.10Q16.82 13.61 16.25 14.93Q15.69 16.24 14.69 17.24Q13.69 18.25 12.37 18.81Q11.05 19.38 9.55 19.38ZM14.13 6.14L9.23 10.95L9.23 1.32L14.13 6.14Z"/></svg></div>
- </div>
- `);
- }
- function updateCurrentTime(t) {
- currentUnix = Math.max(startUnix, Math.min(t, maxUnix));
- slider.slider('option', 'value', currentUnix);
- }
- async function waitForSelector(selector) {
- let result;
- do {
- if(result = document.querySelector(selector))
- return result;
- await delay(100);
- } while(1);
- }
- const debounce = (() => {
- const debounceList = {};
- async function debounceSub(name, interval) {
- while(true) {
- if(debounceList[name]) {
- debounceList[name]();
- delete debounceList[name];
- } else return;
- await delay(interval);
- }
- }
- return (name, method, interval) => {
- const runSub = !debounceList[name];
- debounceList[name] = method;
- if(runSub) debounceSub(name, interval);
- }
- })();
- async function doInit() {
- console.log('Playback Init');
- let obs = new MutationObserver(e => {
- if(e[0].addedNodes.length) {
- setupPlaybackToggle();
- }
- });
- obs.observe(await waitForSelector('#shortcut-menu'), {childList: true});
- setupPlaybackToggle();
- playbackHiddenPosts[0].id = 'playbackHiddenPosts-0';
- document.head.appendChild(playbackHiddenPosts[0]);
- appendStyle();
- await fourChanXInitFinished;
- [...$qa('.postContainer')].forEach(pc => {
- posts[pc.dataset.fullID] = getPostData(pc.dataset.fullID);
- });
- document.addEventListener('ThreadUpdate', e => {
- if(e.detail && e.detail.newPosts && e.detail.newPosts.length) {
- for(let postID of e.detail.newPosts) {
- posts[postID] = getPostData(postID);
- }
- updatePostVisibility();
- }
- });
- setupPlaybackUI();
- startUnix = parseInt($('.opContainer .dateTime').attr('data-utc'));
- currentUnix = isArchived() ? parseInt(Object.values(posts).map(p => p.timestamp).sort((a,b) => b-a)[0]):moment().unix();
- maxUnix = currentUnix;
- console.log('start', startUnix, 'current', currentUnix, 'max', maxUnix);
- function renderPlayback(e, ui) {
- currentUnix = ui.value;
- debounce('renderPlaybackTimestamp', () => {
- let hoverTimestamp = moment.unix(currentUnix)
- .format('yyyy/MM/DD HH:mm:ss');
- handle.attr('tooltip', hoverTimestamp);
- }, 16);
- debounce('renderPlayback', () => {
- updateDateTimeDisplay(currentUnix);
- updatePostVisibility();
- }, 250);
- }
- let updatePreviewSub;
- async function updatePreview() {
- while(true) {
- if(updatePreviewSub) {
- updatePreviewSub();
- } else return;
- await delay(16);
- }
- }
- slider = $('#playbackSlider').slider({
- min: startUnix,
- value: currentUnix,
- max: maxUnix,
- start: (e, ui) => {
- scrubbing = true;
- slider.addClass('ui-state-active');
- },
- stop: (e, ui) => {
- scrubbing = false;
- slider.removeClass('ui-state-active');
- },
- animate: 0,
- change: renderPlayback,
- slide: renderPlayback
- }).on('mousemove', e => {
- /*let runUpdatePreview = !updatePreviewSub;
- updatePreviewSub =*/
- debounce('updatePreview', () => {
- let rect = slider[0].getBoundingClientRect(),
- fraction = (e.clientX - rect.left)/rect.width,
- hoverTimestamp = Math.round(fraction*(maxUnix - startUnix)) + startUnix;
- hoverTimestamp = Math.max(Math.min(hoverTimestamp, maxUnix), startUnix);
- hoverTimestamp = moment.unix(hoverTimestamp)
- .format('yyyy/MM/DD HH:mm:ss');
- preview.attr('tooltip', hoverTimestamp);
- let style = `left: ${e.clientX - rect.left}px`;
- preview[0].style = style;
- //updatePreviewSub = null;
- }, 16);
- //if(runUpdatePreview) updatePreview();
- });
- let handle = $('#playbackUI .ui-slider-handle');
- slider.append('<div id="timestampPreview"></div>');
- let preview = $('#timestampPreview');
- updatePlayback();
- $('#playbackPauseResume').click(e => {
- $(e.target).toggleClass('pause');
- playing = !playing;
- });
- let playbackInputContainer = $('#playbackInputContainer');
- let playbackDisplay = $('#playbackDisplay').click(e => {
- let unit = e.target.dataset.unit;
- let m = moment.unix(currentUnix);
- [...$qa('#playbackInputContainer input')].forEach(e => (e.value = m.format(e.dataset.unit)));
- swapTimeDisplayAndInput();
- let focusElement;
- if(unit) focusElement = $q('#playbackInputContainer [data-unit="'+unit+'"]');
- else focusElement = $q('#playbackInputYear');
- focusElement.focus();
- focusElement.setSelectionRange(0, focusElement.maxLength);
- });
- function swapTimeDisplayAndInput() {
- playbackInputContainer.toggleClass('playbackHidden');
- playbackDisplay.toggleClass('playbackHidden');
- seeking = !seeking;
- }
- function submitInput() {
- let date = [...$qa('#playbackInputDate input')].map(e => e.value.padStart(e.maxLength, '0')).join('/'),
- time = [...$qa('#playbackInputTime input')].map(e => e.value.padStart(e.maxLength, '0')).join(':'),
- m = moment(date + ' ' + time, 'yyyy/MM/DD HH:mm:ss');
- updateCurrentTime(m.unix());
- swapTimeDisplayAndInput();
- }
- function updatePlaybackSpeedDisplay() {
- let n = (Math.round(playbackSpeed*100)/100)+'x';
- playbackSpeedDisplay.html(n);
- }
- let playbackSpeedInput = $('#playbackSpeedInput').on('keyup', e => {
- let isNumber = /^[\d.]$/.test(e.key);
- if(/^[^\d\.]$/.test(e.key) && !e.ctrlKey) {
- e.preventDefault();
- }
- if(e.key == 'Escape') swapSpeedDisplayAndInput();
- if(e.key == 'Enter') {
- let newSpeed;
- try {
- newSpeed = parseFloat(playbackSpeedInput[0].value);
- } catch(e) { newSpeed = 1; }
- playbackSpeed = newSpeed;
- console.log('newSpeed', playbackSpeed);
- updatePlaybackSpeedDisplay();
- swapSpeedDisplayAndInput();
- }
- });
- let playbackSpeedDisplay = $('#playbackSpeedDisplay').click(e => {
- playbackSpeedInput[0].value = playbackSpeed;
- swapSpeedDisplayAndInput();
- playbackSpeedInput.focus();
- playbackSpeedInput[0].setSelectionRange(0, playbackSpeedInput[0].value.length);
- });
- function swapSpeedDisplayAndInput() {
- playbackSpeedDisplay.toggleClass('playbackHidden');
- playbackSpeedInput.toggleClass('playbackHidden');
- }
- $('#playbackSkipBack').click(e => {
- updateCurrentTime(currentUnix - 5);
- });
- $('#playbackSkipAhead').click(e => {
- updateCurrentTime(currentUnix + 5);
- });
- let keydownElement;
- $('#playbackInputContainer input').on('keydown keyup', e => {
- if(e.type == 'keydown') {
- keydownElement = e.target;
- return;
- }
- if(e.target != keydownElement) {
- return;
- }
- let isNumber = /^\d$/.test(e.key);
- if(/^[^\d]$/.test(e.key) && !e.ctrlKey) {
- e.preventDefault();
- }
- if(isNumber && e.target.value.length == e.target.maxLength) {
- if(nextInput[e.target.id]) {
- let next = $id(nextInput[e.target.id]);
- next.focus();
- next.setSelectionRange(0, next.value.length);
- } else submitInput();
- }
- if(e.key == 'Enter') submitInput();
- if(e.key == 'Escape') swapTimeDisplayAndInput();
- });
- console.log('Playback Init complete');
- }
- doInit();
- })();