// ==UserScript== // @name AbemaTV Shortcut Key Controller // @namespace knoa.jp // @description AbemaTVでショートカットキーによる操作を可能にします。キーアサインはYouTube準拠。 // @include https://abema.tv/* // @version 2.2.1 // @grant none // @downloadURL 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) => `