Greasy Fork

Greasy Fork is available in English.

AbemaTV Shortcut Key Controller

AbemaTVでショートカットキーによる操作を可能にします。キーアサインはYouTube準拠。

当前为 2017-12-06 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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'):
log(document.activeElement, site.elements.commentTextarea());
          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);
})();