您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
让你恒常地显示油管上的进度条(显示播放时间比例的红色条)。
当前为
// ==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/* // @version 0.10 // @grant none // ==/UserScript== (function(){ const SCRIPTID = 'YouTubeProgressBarPreserver'; const SCRIPTNAME = 'YouTube ProgressBar Preserver'; const DEBUG = false;/* [update] 0.10 adjust color to advertisements. [todo] カスタマイズ(color, height, opacity, 各表示モードでのオンオフ) うっすら時刻表示オプションほしい? [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 INTERVAL = 1000;/*for core.checkUrl*/ const STARTSWITH = [/*for core.checkUrl*/ 'https://www.youtube.com/watch?v=', ]; const RETRY = 10; let site = { targets: { player: () => $('#movie_player'), video: () => $('video[src]'), time: () => $('.ytp-time-display'), }, is: { live: (time) => time.classList.contains('ytp-live'), }, }; let html, elements = {}, timers = {}; let core = { initialize: function(){ html = document.documentElement; 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, RETRY).then(() => { log("I'm ready."); core.appendBar(); }); }, appendBar: function(){ if(elements.bar && elements.bar.isConnected) return; let bar = elements.bar = createElement(core.html.bar()); let progress = elements.progress = bar.firstElementChild; elements.player.appendChild(bar); core.observeTime(elements.time, bar); core.observeVideo(elements.video, progress); }, observeTime: function(time, bar){ let detect = function(time, bar){ if(site.is.live(time)) bar.classList.remove('active'); else bar.classList.add('active'); }; detect(time, bar); let observer = observe(time, function(records){ detect(time, bar); }, {attributes: true}); }, observeVideo: function(video, progress){ progress.style.transform = 'scaleX(0)'; video.addEventListener('timeupdate', function(e){ progress.style.transform = `scaleX(${video.currentTime / video.duration})`; }); }, getTargets: function(targets, retry = 0){ const get = function(resolve, reject, retry){ for(let i = 0, keys = Object.keys(targets), key; key = keys[i]; i++){ let selected = targets[key](); if(selected){ if(selected.length) selected.forEach((s) => s.dataset.selector = key); else selected.dataset.selector = key; elements[key] = selected; }else{ if(--retry < 0) return reject(log(`Not found: ${key}, I give up.`)); log(`Not found: ${key}, retrying... (left ${retry})`); return setTimeout(get, 1000, resolve, reject, retry); } } resolve(); }; return new Promise(function(resolve, reject){ get(resolve, reject, retry); }); }, addStyle: function(name = 'style'){ if(core.html[name] === undefined) return; let style = createElement(core.html[name]()); document.head.appendChild(style); if(elements[name] && elements[name].isConnected) document.head.removeChild(elements[name]); elements[name] = style; }, html: { bar: () => `<div id="${SCRIPTID}-bar"><div id="${SCRIPTID}-progress"></div></div>`, style: () => ` <style type="text/css"> #${SCRIPTID}-bar{ --height: 3px; --background: rgba(255,255,255,.2); --color: #f00; --ad-color: #fc0; --transition-bar: opacity .25s cubic-bezier(0.0,0.0,0.2,1); --transition-progress: transform .25s linear; --z-index: 100; } #${SCRIPTID}-bar{ width: 100%; height: var(--height); background: var(--background); position: absolute; bottom: 0; transition: var(--transition-bar); opacity: 0; z-index: var(--z-index); } #${SCRIPTID}-progress{ width: 100%; height: var(--height); background: var(--color); transition: var(--transition-progress); transform-origin: 0 0; } .ad-interrupting/*advertisement*/ #${SCRIPTID}-progress{ background: var(--ad-color); } .ytp-autohide #${SCRIPTID}-bar.active{ opacity: 1; } </style> `, }, }; const setTimeout = window.setTimeout, clearTimeout = window.clearTimeout, setInterval = window.setInterval, clearInterval = window.clearInterval, requestAnimationFrame = window.requestAnimationFrame; const alert = window.alert, confirm = window.confirm, getComputedStyle = window.getComputedStyle, fetch = window.fetch; if(!('isConnected' in Node.prototype)) Object.defineProperty(Node.prototype, 'isConnected', {get: function(){return document.contains(this)}}); class Storage{ static key(key){ return (SCRIPTID) ? (SCRIPTID + '-' + key) : key; } static save(key, value, expire = null){ key = Storage.key(key); localStorage[key] = JSON.stringify({ value: value, saved: Date.now(), expire: expire, }); } static read(key){ key = Storage.key(key); if(localStorage[key] === undefined) return undefined; let data = JSON.parse(localStorage[key]); if(data.value === undefined) return data; if(data.expire === undefined) return data; if(data.expire === null) return data.value; if(data.expire < Date.now()) return localStorage.removeItem(key); return data.value; } static delete(key){ key = Storage.key(key); delete localStorage.removeItem(key); } static saved(key){ key = Storage.key(key); if(localStorage[key] === undefined) return undefined; let data = JSON.parse(localStorage[key]); if(data.saved) return data.saved; else return undefined; } } const $ = function(s, f){ let target = document.querySelector(s); if(target === null) return null; return f ? f(target) : target; }; const $$ = function(s){return document.querySelectorAll(s)}; const animate = function(callback, ...params){requestAnimationFrame(() => requestAnimationFrame(() => callback(...params)))}; const wait = function(ms){return new Promise((resolve) => setTimeout(resolve, ms))}; const createElement = function(html = '<span></span>'){ let outer = document.createElement('div'); outer.innerHTML = html; return outer.firstElementChild; }; const observe = function(element, callback, options = {childList: true, attributes: false, characterData: false, subtree: false}){ let observer = new MutationObserver(callback.bind(element)); observer.observe(element, options); return observer; }; const secondsToTime = function(seconds){ let floor = Math.floor, zero = (s) => s.toString().padStart(2, '0'); let h = floor(seconds/3600), m = floor(seconds/60)%60, s = floor(seconds%60); if(h) return h + '時間' + zero(m) + '分' + zero(s) + '秒'; if(m) return m + '分' + zero(s) + '秒'; if(s) return s + '秒'; }; const timeToSeconds = function(time){ let parts = time.split(':').map(p => parseFloat(p)); switch(parts.length){ case(1): return parts[0]; case(2): return parts[0]*60 + parts[1]; case(3): return parts[0]*60*60 + parts[1]*60 + parts[2]; default: return 0; } }; const atLeast = function(min, b){ return Math.max(min, b); }; const atMost = function(a, max){ return Math.min(a, max); }; const between = function(min, b, max){ return Math.min(Math.max(min, b), max); }; const toMetric = function(number, decimal = 1){ switch(true){ case(number < 1e3 ): return (number); case(number < 1e6 ): return (number/1e3 ).toFixed(decimal) + 'K'; case(number < 1e9 ): return (number/1e6 ).toFixed(decimal) + 'M'; case(number < 1e12): return (number/1e9 ).toFixed(decimal) + 'G'; default: return (number/1e12).toFixed(decimal) + 'T'; } }; const log = function(){ if(!DEBUG) return; let l = log.last = log.now || new Date(), n = log.now = new Date(); let error = new Error(), line = log.format.getLine(error), callers = log.format.getCallers(error); //console.log(error.stack); console.log( (SCRIPTID || '') + ':', /* 00:00:00.000 */ n.toLocaleTimeString() + '.' + n.getTime().toString().slice(-3), /* +0.000s */ '+' + ((n-l)/1000).toFixed(3) + 's', /* :00 */ ':' + line, /* caller.caller */ (callers[2] ? callers[2] + '() => ' : '') + /* caller */ (callers[1] || '') + '()', ...arguments ); }; log.formats = [{ name: 'Firefox Scratchpad', detector: /MARKER@Scratchpad/, getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1], getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm), }, { name: 'Firefox Console', detector: /MARKER@debugger/, getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1], getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm), }, { name: 'Firefox Greasemonkey 3', detector: /\/gm_scripts\//, getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1], getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm), }, { name: 'Firefox Greasemonkey 4+', detector: /MARKER@user-script:/, getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1] - 500, getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm), }, { name: 'Firefox Tampermonkey', detector: /MARKER@moz-extension:/, getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1] - 6, getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm), }, { name: 'Chrome Console', detector: /at MARKER \(<anonymous>/, getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)?$/)[1], getCallers: (e) => e.stack.match(/[^ ]+(?= \(<anonymous>)/gm), }, { name: 'Chrome Tampermonkey', detector: /at MARKER \(chrome-extension:.*?\/userscript.html\?id=/, getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)?$/)[1] - 6, getCallers: (e) => e.stack.match(/[^ ]+(?= \(chrome-extension:)/gm), }, { name: 'Chrome Extension', detector: /at MARKER \(chrome-extension:/, getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)?$/)[1], getCallers: (e) => e.stack.match(/[^ ]+(?= \(chrome-extension:)/gm), }, { name: 'Edge Console', detector: /at MARKER \(eval/, getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)$/)[1], getCallers: (e) => e.stack.match(/[^ ]+(?= \(eval)/gm), }, { name: 'Edge Tampermonkey', detector: /at MARKER \(Function/, getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)$/)[1] - 4, getCallers: (e) => e.stack.match(/[^ ]+(?= \(Function)/gm), }, { name: 'Safari', detector: /^MARKER$/m, getLine: (e) => 0,/*e.lineが用意されているが最終呼び出し位置のみ*/ getCallers: (e) => e.stack.split('\n'), }, { name: 'Default', detector: /./, getLine: (e) => 0, getCallers: (e) => [], }]; log.format = log.formats.find(function MARKER(f){ if(!f.detector.test(new Error().stack)) return false; //console.log('////', f.name, 'wants', 0/*line*/, '\n' + new Error().stack); return true; }); const time = function(label){ if(!DEBUG) return; const BAR = '|', TOTAL = 100; switch(true){ case(label === undefined):/* time() to output total */ let total = 0; Object.keys(time.records).forEach((label) => total += time.records[label].total); Object.keys(time.records).forEach((label) => { console.log( BAR.repeat((time.records[label].total / total) * TOTAL), label + ':', (time.records[label].total).toFixed(3) + 'ms', '(' + time.records[label].count + ')', ); }); time.records = {}; break; case(!time.records[label]):/* time('label') to create and start the record */ time.records[label] = {count: 0, from: performance.now(), total: 0}; break; case(time.records[label].from === null):/* time('label') to re-start the lap */ time.records[label].from = performance.now(); break; case(0 < time.records[label].from):/* time('label') to add lap time to the record */ time.records[label].total += performance.now() - time.records[label].from; time.records[label].from = null; time.records[label].count += 1; break; } }; time.records = {}; core.initialize(); if(window === top && console.timeEnd) console.timeEnd(SCRIPTID); })();