您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
AbemaTVでショートカットキーによる操作を可能にします。キーアサインはYouTube準拠。
当前为
// ==UserScript== // @name AbemaTV Shortcut Key Controller // @namespace knoa.jp // @description AbemaTVでショートカットキーによる操作を可能にします。キーアサインはYouTube準拠。 // @include https://abema.tv/* // @version 2.2.1 // @grant none // ==/UserScript== // console.log('AbemaTV? => hireMe()'); (function(){ const SCRIPTNAME = 'ShortcutKeyController'; const DEBUG = false;/* [update] コメント欄にフォーカス中に[Esc]で、フォーカスを外すようにしました。 [to do] Escでコメント欄からフォーカスを外したい パネル上でもマウスホイールしたい? ホワイトリストじゃなくてscrollableかどうかで判定できるのでは>>むずかしそう scrollMaxとscrollとか使っても無理なんだっけ?>>小さな子要素でイベントが発生してしまうからかな?親をたどれば? */ if(window === top && console.time) console.time(SCRIPTNAME); const DUMMY = document.body; let site = { elements: { /* 共通 */ fullscreenButton: function(){let node = $('use[*|href*="mini_screen.svg"]') || $('use[*|href*="_screen.svg"]')/*タイムシフトのbuttonにaria-labelがないので*/; return node ? node.parentNode.parentNode : DUMMY;}, volumeSlider: function(){let node = $('button[aria-label="音声オンオフ切り替え"]'); return node ? node.previousSibling.firstElementChild.firstElementChild : DUMMY;}, muteButton: function(){let node = $('button[aria-label="音声オンオフ切り替え"]'); return node ? node : DUMMY;}, /* リアルタイム */ channelButton: function(){let node = $('button[aria-label="放送中の裏番組"]'); return node ? node : DUMMY;}, commentButton: function(){let node = $('use[*|href^="/images/icons/comment.svg"]'); return node ? node.parentNode.parentNode : DUMMY;}, programButton: function(){let node = $('button[aria-label^="フルスクリーン"] + div + div > div > div'); return node ? node : DUMMY;}, commentTextarea: function(){let node = $('textarea[placeholder="コメントを入力"]'); return node ? node : DUMMY;}, footer: function(){let node = $('button[aria-label^="フルスクリーン"]'); return node ? node.parentNode.parentNode : DUMMY;}, closer: function(){let node = $('form:not([role="search"])'); return node ? node.parentNode.parentNode.parentNode.nextElementSibling : DUMMY;}, /* タイムシフト */ stopper: function(){let node = $('use[*|href^="/images/icons/playback.svg"]'); return node ? node.parentNode.parentNode.nextElementSibling : DUMMY;}, playButton: function(){let node = $('use[*|href^="/images/icons/rewind_10.svg"]'); return node ? node.parentNode.parentNode.previousElementSibling : DUMMY;}, rewindButton: function(){let node = $('use[*|href^="/images/icons/rewind_10.svg"]'); return node ? node.parentNode.parentNode : DUMMY;}, advancesButton: function(){let node = $('use[*|href^="/images/icons/advances_10.svg"]'); return node ? node.parentNode.parentNode : DUMMY;}, }, isCommentPaneHidden: function(){ let form = $('form:not([role="search"])'); return (form) ? (form.parentNode.parentNode.getAttribute('aria-hidden') === 'true') : false; }, modifyVolume: function(e){ let slider = site.elements.volumeSlider(), rect = slider.getBoundingClientRect(); let volume = parseInt(slider.firstElementChild.style.height) / rect.height; switch(e.deltaMode){ case(WheelEvent.DOM_DELTA_PIXEL):/*ヌルヌル*/ volume += -(e.deltaY/1000); break; case(WheelEvent.DOM_DELTA_LINE):/*カクカク*/ default: volume += (0 < e.deltaY) ? -(1/10) : (1/10); break; } slider.dispatchEvent(new MouseEvent('mousedown', { clientX: rect.x + (rect.width/2), clientY: rect.y + (rect.height * (1 - volume)), bubbles: true, })); }, assign: function(e){ switch(true){ case(location.href.startsWith('https://abema.tv/now-on-air/')): return core.realtime(e); case(location.href.startsWith('https://abema.tv/channels/')): case(location.href.startsWith('https://abema.tv/video/watch/')): case(location.href.startsWith('https://abema.tv/video/episode/')): return core.timeshift(e); } }, }; let globals = {}, elements = {}; let core = { initialize: function(){ window.addEventListener('wheel', site.assign, true); window.addEventListener('keydown', site.assign, true); }, /* リアルタイム */ realtime: function(e){ switch(true){ /* 音量 */ case(e.type === 'wheel'): /* あらゆる場所でのイベントを拾ってwindow.addEventListenerで一括処理する代償をここで支払う */ for(let target = e.target; target; target = target.parentNode){ //log(target, target.scrollTop, target.scrollTopMax); if(e.target.localName === 'button' || [site.elements.closer(), site.elements.footer()].includes(target)){ site.modifyVolume(e); return e.preventDefault(); } } return; /* コメント入力欄フォーカスを外す */ case(e.key == 'Escape'): if(document.activeElement === site.elements.commentTextarea()){ document.activeElement.blur(); return e.preventDefault(); } break; /* 以下、テキスト入力中は反応しない */ case(['input', 'textarea'].includes(document.activeElement.localName)): return; /* Alt/Shift/Ctrl/Metaキーが押されていたら反応しない */ case(e.altKey || e.shiftKey || e.ctrlKey || e.metaKey): return; /* コメント入力欄フォーカス */ case(e.key == 'k'): case(e.key == ' '): case(e.key == 'Enter'): /* コメント欄が表示されていなければあらかじめ表示しておく */ if(site.isCommentPaneHidden()) site.elements.commentButton().click(); site.elements.commentTextarea().focus(); return e.preventDefault(); /* コメント */ case(e.key == 'c'): if(site.isCommentPaneHidden()) site.elements.commentButton().click(); else site.elements.closer().click(); return e.preventDefault(); /* 裏番組一覧 */ case(e.key == 'n'): site.elements.channelButton().click(); return e.preventDefault(); /* 番組情報 */ case(e.key == 'i'): site.elements.programButton().click(); return e.preventDefault(); /* 10秒戻る(20秒かけて追いつく) */ case(e.key == 'j'): case(e.key == 'ArrowLeft'): const REWIND = 10, CATCHUP = 1.5; let video = document.querySelector('video[src]'); if(!video || video.rewinded) return; let rewind = Math.min(REWIND, video.currentTime); video.rewinded = true; video.currentTime = video.currentTime - rewind; video.playbackRate = CATCHUP; setTimeout(function(){ video.rewinded = false; video.playbackRate = 1; }, (rewind / (CATCHUP - 1))*1000); return e.preventDefault(); /* フルスクリーン */ case(e.key == 'f'): site.elements.fullscreenButton().click(); return e.preventDefault(); /* ミュート */ case(e.key == 'm'): site.elements.muteButton().click(); return e.preventDefault(); /* ヘルプ */ case(e.key == 'h'): case(e.key == '/'): core.toggleHelp('realtime'); return e.preventDefault(); } }, /* タイムシフト */ timeshift: function(e){ switch(true){ /* 音量 */ case(e.type === 'wheel'): if(e.target === site.elements.stopper()){ site.modifyVolume(e); return e.preventDefault(); } return; /* 以下、テキスト入力中は反応しない */ case(['input', 'textarea'].includes(document.activeElement.localName)): return; /* Alt/Shift/Ctrl/Metaキーが押されていたら反応しない */ case(e.altKey || e.shiftKey || e.ctrlKey || e.metaKey): return; /* 再生・停止トグル */ case(e.key == 'k'): case(e.key == ' '): case(e.key == 'Enter'): site.elements.playButton().click(); return e.preventDefault(); /* 10秒戻る */ case(e.key == 'j'): case(e.key == 'ArrowLeft'): site.elements.rewindButton().click(); return e.preventDefault(); /* 10秒進む */ case(e.key == 'l'): case(e.key == 'ArrowRight'): site.elements.advancesButton().click(); return e.preventDefault(); /* フルスクリーン */ case(e.key == 'f'): site.elements.fullscreenButton().click(); return e.preventDefault(); /* ミュート */ case(e.key == 'm'): site.elements.muteButton().click(); return e.preventDefault(); /* ヘルプ */ case(e.key == 'h'): case(e.key == '/'): core.toggleHelp('timeshift'); return e.preventDefault(); } }, toggleHelp: function(type){ core.panel.toggle('help', core.createHelp.bind(null, type)); }, createHelp: function(type){ core.addStyle(); elements.help = createElement(core.html.help(type)); elements.help.querySelector('button.ok').addEventListener('click', core.panel.close.bind(null, 'help')); core.panel.open('help'); }, /* パネル共通 */ panel: { open: function(key){ elements[key].classList.add('hidden'); document.body.appendChild(elements[key]); animate(function(){ elements[key].classList.remove('hidden'); }); if(!globals.listeningKeypress){ globals.listeningKeypress = true; window.addEventListener('keypress', function(e){ if(['input', 'textarea'].includes(document.activeElement.localName)) return; if(elements[key] && e.key === 'Escape') core.panel.close(key); }); } }, close: function(key){ elements[key].classList.add('hidden'); elements[key].addEventListener('transitionend', function(){ if(!elements[key]) return; document.body.removeChild(elements[key]); elements[key] = null; }, {once: true}); }, toggle: function(key, create){ (!elements[key]) ? create() : core.panel.close(key); }, }, addStyle: function(){ let style = createElement(core.html.style()); document.head.appendChild(style); if(elements.style && elements.style.isConnected) document.head.removeChild(elements.style); elements.style = style; }, html: { help: (type) => ` <div class="${SCRIPTNAME} panel" id="${SCRIPTNAME}-help"> <h1>${SCRIPTNAME} ヘルプ</h1> <h2>共通:</h2> <dl> <dt><kbd>[H]</kbd><kbd>[/]</kbd></dt><dd>ヘルプ表示 ([H]elp)</dd> <dt><kbd>[F]</kbd></dt><dd>フルスクリーン ([F]ullscreen)</dd> <dt><kbd>[M]</kbd></dt><dd>ミュート ([M]ute)</dd> <dt><kbd>[マウスホイール]</kbd></dt><dd>音量調整</dd> </dl> <h2${(type === 'realtime') ? '' : ' class="disabled"'}>リアルタイム放送:</h2> <dl> <dt><kbd>[K]</kbd><kbd>[Space]</kbd><kbd>[Enter]</kbd></dt><dd>コメント入力欄フォーカス</dd> <dt><kbd>[Esc]</kbd></dt><dd>コメント入力欄フォーカスを外す</dd> <dt><kbd>[C]</kbd></dt><dd>コメント表示 ([C]omment)</dd> <dt><kbd>[N]</kbd></dt><dd>裏番組一覧 ([N]ow on air)</dd> <dt><kbd>[I]</kbd></dt><dd>番組情報 ([I]nformation)</dd> <dt><kbd>[J]</kbd><kbd>[←]</kbd></dt><dd>10秒戻る(20秒かけて追いつく)</dd> </dl> <h2${(type === 'timeshift') ? '' : ' class="disabled"'}>タイムシフト放送:</h2> <dl> <dt><kbd>[K]</kbd><kbd>[Space]</kbd><kbd>[Enter]</kbd></dt><dd>再生・停止</dd> <dt><kbd>[J]</kbd><kbd>[←]</kbd></dt><dd>10秒戻る</dd> <dt><kbd>[L]</kbd><kbd>[→]</kbd></dt><dd>10秒進む</dd> </dl> <p class="buttons"><button class="ok primary">OK</button></p> </div> `, style: () => ` <style> /* パネル共通 */ body{ overflow: hidden; } .${SCRIPTNAME}.panel{ position: absolute; width: 360px; max-height: 100%;/*小さなウィンドウに対応*/ overflow: auto; left: 50%; bottom: 50%; transform: translate(-50%, 50%); z-index: 100; background: rgba(0,0,0,.75); transition: 500ms ease; padding: 5px 0; } .${SCRIPTNAME}.panel.hidden{ bottom: 0; transform: translate(-50%, 100%) !important; } .${SCRIPTNAME} h1, .${SCRIPTNAME} h2, .${SCRIPTNAME} h3, .${SCRIPTNAME} h4, .${SCRIPTNAME} legend, .${SCRIPTNAME} ul, .${SCRIPTNAME} ol, .${SCRIPTNAME} dl, .${SCRIPTNAME} code, .${SCRIPTNAME} p{ color: rgba(255,255,255,1); font-size: 14px; padding: 2px 10px; line-height:20px; } .${SCRIPTNAME} header{ display: flex; } .${SCRIPTNAME} header h1{ flex: 1; } .${SCRIPTNAME}.panel > p.buttons{ text-align: right; padding: 5px 10px; } .${SCRIPTNAME}.panel > p.buttons button{ width: 120px; padding: 5px 10px; margin-left: 10px; border-radius: 5px; color: rgba(255,255,255,1); background: rgba(64,64,64,1); border: 1px solid rgba(255,255,255,1); } .${SCRIPTNAME}.panel > p.buttons button.primary{ font-weight: bold; background: rgba(0,0,0,1); } .${SCRIPTNAME}.panel > p.buttons button:hover, .${SCRIPTNAME}.panel > p.buttons button:focus{ background: rgba(128,128,128,.75); } ${SCRIPTNAME} .template{ display: none !important; } /* ヘルプパネル */ #${SCRIPTNAME}-help{ width: 420px; } #${SCRIPTNAME}-help dl{ display: flex; flex-wrap: wrap; } #${SCRIPTNAME}-help dl dt{ width: 160px; margin: 2.5px 10px 2.5px 0; background: rgba(0,0,0,.5); border-radius: 5px; } #${SCRIPTNAME}-help dl dt kbd{ margin-left: 5px; } #${SCRIPTNAME}-help dl dd{ width: 230px; margin: 2.5px 0; } #${SCRIPTNAME}-help h2.disabled, #${SCRIPTNAME}-help h2.disabled + dl{ opacity: .5; } </style> `, }, }; let $ = function(s){return document.querySelector(s)}; let animate = function(callback, ...params){requestAnimationFrame(() => requestAnimationFrame(() => callback(...params)))}; let createElement = function(html){ let outer = document.createElement('div'); outer.innerHTML = html; return outer.firstElementChild; }; let log = (DEBUG) ? console.log.bind(null, SCRIPTNAME + ':') : function(){}; core.initialize(); if(window === top && console.timeEnd) console.timeEnd(SCRIPTNAME); })();