// ==UserScript== // @name 4chan X Thread Playback // @namespace VSJPlus // @license GNU GPLv3 // @description Plays back threads on 4chan X // @version 1.0.2 // @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 // @downloadURL none // ==/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; } #playbackSlider:hover { background: #c4c4c4; } #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: -2px; } #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 { font-size: 20px; font-weight: bold; color: #ccc; cursor: pointer; transition: color 100ms; position: relative; width: 18px; height: 27px; } #playbackSkipBack:before, #playbackSkipAhead:before { top: -1px; } #playbackSkipBack { transform-origin: 9px 15px; margin-left: 5px; } #playbackSkipAhead { transform-origin: 10px 15px; } #playbackSkipBack:hover { color: #fff; } #playbackSkipAhead:hover { color: #fff; } #playbackSkipBack:before { content: '⭯'; position: absolute; transition: transform 100ms; } #playbackSkipBack:active:before { transform: rotate(-25deg); } #playbackSkipAhead:before { content: '⭮'; position: absolute; transition: transform 100ms; } #playbackSkipAhead:active:before { transform: rotate(25deg); } #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]:hover:after { 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-weight: normal; font-size: 10px; text-align: center; z-index: 1; pointer-events: none; animation: tooltipFade 800ms; color: #ccc; } @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() { maxUnix = isArchived() ? parseInt($q('.thread > .postContainer:last-of-type .dateTime').dataset.utc):moment().unix(); if(playing) currentUnix++; currentUnix = Math.min(currentUnix, maxUnix); slider.slider('option', 'max', maxUnix); slider.slider('option', 'value', currentUnix); } let lastUpdate, playbackSpeed = 1, correction = 0; const adjustedInterval = () => { let now = Date.now(), increment = 1000/playbackSpeed; if(!lastUpdate) lastUpdate = now - increment; correction = increment - (now - lastUpdate - correction); lastUpdate = now; return increment + correction; } async function updatePlayback() { await delay(1000 - Date.now()%1000); while(true) { if(!scrubbing) updatePlaybackSub(); await delay(adjustedInterval()); } } const playbackHiddenPosts = document.createElement('style'); let lastHiddenPosts; async function updatePostVisibility() { let css = posts.filter(p => p.timestamp > currentUnix).map(p => p.selectors), newPosts = lastHiddenPosts != css.length; if(!newPosts) return; lastHiddenPosts = css.length; css = css.join(', '); css += '{ display: none !important; }'; let scrollToBottom = false, docEl = document.documentElement; if(newPosts && autoScroll && (docEl.offsetHeight - (docEl.scrollTop + window.innerHeight)) < 100) { scrollToBottom = true; } playbackHiddenPosts.innerHTML = css; 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', ''); 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'); await fourChanXInitFinished; toggle.removeClass('loading'); } $(checkbox).click(() => { let checked = document.querySelector('#playbackToggleCheckbox').checked; $(document.documentElement).toggleClass('playbackEnabled'); togglePlay(checked); }); } function setupPlaybackUI() { $q('#header-bar').insertAdjacentHTML('beforeend', `