// ==UserScript== // @name YouTube ProgressBar Preserver // @name:ja YouTube ProgressBar Preserver // @name:zh-CN YouTube ProgressBar Preserver // @description It preserves YouTube's progress bar always visible even if the controls are hidden. // @description:ja YouTubeのプログレスバー(再生時刻の割合を示す赤いバー)を、隠さず常に表示させるようにします。 // @description:zh-CN 让你恒常地显示油管上的进度条(显示播放时间比例的红色条)。 // @namespace knoa.jp // @include https://www.youtube.com/* // @include https://www.youtube-nocookie.com/embed/* // @exclude https://www.youtube.com/live_chat* // @exclude https://www.youtube.com/live_chat_replay* // @version 1.0.2 // @grant none // @downloadURL https://update.greasyfork.icu/scripts/394512/YouTube%20ProgressBar%20Preserver.user.js // @updateURL https://update.greasyfork.icu/scripts/394512/YouTube%20ProgressBar%20Preserver.meta.js // ==/UserScript== (function(){ const SCRIPTID = 'YouTubeProgressBarPreserver'; const SCRIPTNAME = 'YouTube ProgressBar Preserver'; const DEBUG = false;/* [update] No updates on code. Just confirmed to work. [bug] [todo] [research] timeupdateの間隔ぶんだけ遅れてしまうのはうまく改善できるかどうか timeupdateきっかけで250ms(前回との差?)をキープするような仕組みでいける? もっとも、時間の短い広告時くらいしか知覚できないけど。 [memo] YouTubeによって隠されているときはオリジナルのバーは更新されないので、独自に作るほうがラク。 0.9完成後、youtube progressbar で検索したところすでに存在していることを発見\(^o^)/ https://addons.mozilla.org/ja/firefox/addon/progress-bar-for-youtube/ カスタマイズできるが、生放送に対応していない。プログレスが最低0.5秒単位でtransitionもない。 */ if(window === top && console.time) console.time(SCRIPTID); const MS = 1, SECOND = 1000*MS, MINUTE = 60*SECOND, HOUR = 60*MINUTE, DAY = 24*HOUR, WEEK = 7*DAY, MONTH = 30*DAY, YEAR = 365*DAY; const INTERVAL = 1*SECOND;/*for core.checkUrl*/ const SHORTDURATION = 4*MINUTE / 1000;/*short video should have transition*/ const STARTSWITH = [/*for core.checkUrl*/ 'https://www.youtube.com/watch?', 'https://www.youtube.com/embed/', 'https://www.youtube-nocookie.com/embed/', ]; let site = { targets: { player: () => $('.html5-video-player'), video: () => $('video[src]'), time: () => $('.ytp-time-display'), }, is: { live: (time) => time.classList.contains('ytp-live'), }, }; let elements = {}, timers = {}; let core = { initialize: function(){ elements.html = document.documentElement; elements.html.classList.add(SCRIPTID); core.checkUrl(); core.addStyle(); }, checkUrl: function(){ let previousUrl = ''; timers.checkUrl = setInterval(function(){ if(document.hidden) return; /* The page is visible, so... */ if(location.href === previousUrl) return; else previousUrl = location.href; /* The URL has changed, so... */ if(STARTSWITH.some(url => location.href.startsWith(url)) === false) return; /* This page should be modified, so... */ core.ready(); }, INTERVAL); }, ready: function(){ core.getTargets(site.targets).then(() => { log("I'm ready."); core.appendBar(); core.observeTime(); core.observeVideo(); }).catch(e => { console.error(`${SCRIPTID}:${e.lineNumber} ${e.name}: ${e.message}`); }); }, appendBar: function(){ if(elements.bar && elements.bar.isConnected) return; let bar = elements.bar = createElement(html.bar()); let progress = elements.progress = bar.firstElementChild; let buffer = elements.buffer = bar.lastElementChild; elements.player.appendChild(bar); }, observeTime: function(){ /* detect live for hiding the bar */ let time = elements.time, bar = elements.bar; let detect = function(time, bar){ if(site.is.live(time)) bar.classList.remove('active'); else bar.classList.add('active'); }; detect(time, bar); if(time.isObservingAttributes) return; time.isObservingAttributes = true; let observer = observe(time, function(records){ detect(time, bar); }, {attributes: true}); }, observeVideo: function(){ let video = elements.video, progress = elements.progress, buffer = elements.buffer; if(video.isObservingForProgressBar) return; video.isObservingForProgressBar = true; if(video.duration < SHORTDURATION) progress.classList.add('transition'); progress.style.transform = 'scaleX(0)'; video.addEventListener('durationchange', function(e){ if(video.duration < SHORTDURATION) progress.classList.add('transition'); else progress.classList.remove('transition'); }); video.addEventListener('timeupdate', function(e){ progress.style.transform = `scaleX(${video.currentTime / video.duration})`; }); let renderBuffer = function(e){ for(let i = video.buffered.length - 1; 0 <= i; i--){ if(video.currentTime < video.buffered.start(i)) continue; buffer.style.transform = `scaleX(${video.buffered.end(i) / video.duration})`; break; } }; video.addEventListener('progress', renderBuffer); video.addEventListener('seeking', renderBuffer); }, getTarget: function(selector, retry = 10, interval = 1*SECOND){ const key = selector.name; const get = function(resolve, reject){ let selected = selector(); if(selected && selected.length > 0) selected.forEach((s) => s.dataset.selector = key);/* elements */ else if(selected instanceof HTMLElement) selected.dataset.selector = key;/* element */ else if(--retry) return log(`Not found: ${key}, retrying... (${retry})`), setTimeout(get, interval, resolve, reject); else return reject(new Error(`Not found: ${selector.name}, I give up.`)); elements[key] = selected; resolve(selected); }; return new Promise(function(resolve, reject){ get(resolve, reject); }); }, getTargets: function(selectors, retry = 10, interval = 1*SECOND){ return Promise.all(Object.values(selectors).map(selector => core.getTarget(selector, retry, interval))); }, addStyle: function(name = 'style'){ if(html[name] === undefined) return; let style = createElement(html[name]()); document.head.appendChild(style); if(elements[name] && elements[name].isConnected) document.head.removeChild(elements[name]); elements[name] = style; }, }; const html = { bar: () => `