YT快速鍵hotkeys

YT快速鍵控制功能,說明鍵:[?]Adds more keyboard shortcuts for YouTube. help for [?] or SHIFT+[/] key.

目前为 2024-02-25 提交的版本。查看 最新版本

// ==UserScript==
// @name         YT快速鍵hotkeys
// @namespace    https://greasyfork.org/users/4839
// @version      1.0.5
// @license      AGPLv3
// @author       jcunews,leadra
// @description  YT快速鍵控制功能,說明鍵:[?]Adds more keyboard shortcuts for YouTube. help for [?] or SHIFT+[/] key.
// @match        *://www.youtube.com/*
// @grant        none
// @run-at       document-start
// ==/UserScript==
//原作者https://greasyfork.org/en/users/85671-jcunews

 //速度提示框
const SHOW_NOTIFICATIONS = true;
const NOTIFICATION_DURATION_MILLIS = 500;

var lastToastElement = null;
function showNotification(message) {
    if (!SHOW_NOTIFICATIONS) {
        return;
    }
    if (lastToastElement !== null) { // delete if still visible
        lastToastElement.remove();
        lastToastElement = null;
    }

    const toast = document.createElement('tp-yt-paper-toast');
    toast.innerText = message;
    toast.classList.add('toast-open');

    const styleProps = {
        outline: 'none',
        position: 'fixed',
        left: '0%',
        bottom: '2%',
        maxWidth: '100px',
        maxHeight: '48px',
        zIndex: '2202',
        opacity: '1',
    };
    for (const prop in styleProps) {
        toast.style[prop] = styleProps[prop];
    }

    document.body.appendChild(toast);
    lastToastElement = toast;

    // needed otherwise the notification won't show
    setTimeout(() => {
        toast.style.display = 'block';
    }, 0);

    // preserves the animation
    setTimeout(() => {
        toast.style.transform = 'none';
    }, 0);

    setTimeout(() => {
        toast.style.display = 'none';
    }, Math.max(0, NOTIFICATION_DURATION_MILLIS));
}


(ch => {

  //=== CONFIGURATION BEGIN
  /*
   `key`是關鍵名稱。 則必須在大寫中。
   `mods`是零或最多3個修飾符鍵字符(以任何順序): `A`=Alt, `C`=Control, `S`=Shift. 角色案例被忽略。
     例如 “”(無修飾符鍵), "s" (Shift), "Cs" (Control+Shift), "aSc" (Control+Shift+Alt).
   `desc'是否將添加到YouTube的Hotkey List彈出窗口中(可通過`?'或`shift+/`鍵訪問)。
     如果此屬性為空或不存在,則HotKey將不會包含在YouTube的Hotkey List彈出窗口中。
   "keys"是鍵盤鍵的可選自定義文本表示形式,可用於表示多個熱鍵。
   `func`是用激活的熱鍵對象執行的JavaScript函數,作為第一個參數。
   */
  /*
  `key` is the key name. If it's a letter, it must be in uppercase.
  `mods` is a zero or up to 3 modifier key characters (in any order): `A`=Alt, `C`=Control, `S`=Shift. Character case is ignored.
    e.g. "" (no modifier key), "s" (Shift), "Cs" (Control+Shift), "aSc" (Control+Shift+Alt).
  `desc` is the hotkey description which will be added onto YouTube's Hotkey List Popup (accessible via `?` or `SHIFT+/` key).
    If this property is empty or doesn't exist, the hotkey won't be included in YouTube's Hotkey List Popup.
  `keys` is an optional custom text representation for the keyboard keys which is useful to represent multiple hotkeys.
  `func` is the JavaScript function to execute with the activated hotkey object as the first argument.
  */

  var hotkeys = [
    {key: "`", mods: "", desc: "左邊欄位Toggle guide / sidebar", func: a => eleClick('#guide-button')},
    {key: ";", mods: "", desc: "焦點搜尋框Focus channel search box", func: a => eleClick('#tabs-container :is(ytd-expandable-tab-renderer,.ytd-expandable-tab-renderer):has(form[action*="/search"]) button.yt-icon-button')},
    {key: "S", mods: "", desc: "截圖Screenshot  (搭配腳本https://greasyfork.org/zh-TW/scripts/466259-youtube-video-screenshot)", func: a => eleClick('#yt-ss-btn')},
    {key: "Y", mods: "", desc: "按喜歡Toggle like video", func: a => eleClick(['#segmented-like-button button', ':is(#info, #description-and-actions, #actions) #menu ytd-toggle-button-renderer:nth-of-type(1) button#button', '#info #menu #top-level-buttons-computed ytd-toggle-button-renderer:nth-of-type(1) button#button', 'like-button-view-model button'])},
    {key: "W", mods: "", desc: "Tabview資訊 (搭配腳本https://github.com/cyfung1031/Tabview-Youtube)", func: a => eleClick('#tab-btn1')},
    {key: "E", mods: "", desc: "Tabview留言 (搭配腳本https://github.com/cyfung1031/Tabview-Youtube)", func: a => eleClick('#tab-btn3')},
    {key: "W", mods: "S", desc: "Tabview清單 (搭配腳本https://github.com/cyfung1031/Tabview-Youtube)", func: a => eleClick('#tab-btn2')},
    {key: "E", mods: "S", desc: "Tabview影片 (搭配腳本https://github.com/cyfung1031/Tabview-Youtube)", func: a => eleClick('#tab-btn4')},

    {key: "A", mods: "", desc: "倒退Rewind video by 1 seconds", func: a => videoSeekBy(-1)},
    {key: "D", mods: "", desc: "前進Fast forward video by 1 seconds", func: a => videoSeekBy(1)},
    {key: "A", mods: "S", desc: "開始Rewind video by 99999 seconds", func: a => videoSeekBy(-99999)},
    {key: "D", mods: "S", desc: "最後Fast forward video by 99999 seconds", func: a => videoSeekBy(99999)},

    {key: "R", mods: "", desc: "聊天室/章節Toggle replay chat or chapter list", func: toggleChatChap},
    {key: "~", mods: "S", desc: "首頁Go to YouTube home page", func: a => eleClick('a#logo')},
    {key: "V", mods: "S", desc: "版主頻道Go to user/channel video page", func: a => navUser("Videos", "videos")},
    {key: "F", mods: "S", desc: '你的內容Go to Feeds ("You") page', func: a => eleClick('a[href="/feed/you"]') || (location.href = "/feed/you")},
    //{key: "C", mods: "S", desc: "Select preferred subtitle language", func: selectCaption},

    {key: "G", mods: "A", desc: "提高解析度Decrease video quality", func: selectQuality},
    {key: "H", mods: "A", desc: "降低解析度Increase video quality", func: selectQuality},
    {key: "Y", mods: "A", desc: "自動解析度Set video quality to auto", func: selectQuality},

    {key: "<", mods: "S", desc: "加速(上限  2X )Decrease video playback speed by 0.25", func: a => adjustSpeed(-1)},
    //{key: "Z", mods: "", desc: "加速Decrease video playback speed by 0.25", func: a => adjustSpeed(-1)},
    {key: ">", mods: "S", desc: "減速(下限0.25X)Increase video playback speed by 0.25", func: a => adjustSpeed(1)},
    //{key: "X", mods: "", desc: "減速Increase video playback speed by 0.25", func: a => adjustSpeed(1)},
    {key: "M", mods: "S", desc: "恢復原速Increase video playback speed by 1", func: a => adjustSpeed2()},
    //{key: "C", mods: "", desc: "恢復原速Increase video playback speed by 1", func: a => adjustSpeed2()},

    {key: "J", mods: "A", func: a => videoSeekChapter(-1), desc: "上一章節Seek to previous chapter"},
    {key: "L", mods: "A", func: a => videoSeekChapter(1), desc: "下一章節Seek to next chapter"},
//
    {key: "0", mods: "",desc: "禁用 0-9 跳進度N%", func: a => videoSeekTo(none)},
    {key: "1", mods: "", func: a => videoSeekTo(none)},
    {key: "2", mods: "", func: a => videoSeekTo(none)},
    {key: "3", mods: "", func: a => videoSeekTo(none)},
    {key: "4", mods: "", func: a => videoSeekTo(none)},
    {key: "5", mods: "", func: a => videoSeekTo(none)},
    {key: "6", mods: "", func: a => videoSeekTo(none)},
    {key: "7", mods: "", func: a => videoSeekTo(none)},
    {key: "8", mods: "", func: a => videoSeekTo(none)},
    {key: "9", mods: "",desc: "禁用 0-9 跳進度N%", func: a => videoSeekTo(none)},

    //{key: "H", mods: "", desc: "Share video", func: a => eleClick([':is(#info, #description-and-actions) #menu ytd-button-renderer:nth-of-type(1) button#button,ytd-watch-metadata #menu ytd-button-renderer button:has(div[style*="share"])', 'ytd-watch-metadata #menu ytd-button-renderer button[aria-label="Share"]'])},
    //{key: "N", mods: "", desc: "Download video", func: a => eleClick(['ytd-watch-metadata #menu ytd-button-renderer button:has(div[style*="download"])', '.ytd-download-button-renderer button'])},
    //{key: "Q", mods: "S", desc: "Toggle YouTube video controls", func: toggleYtVideoControls},
    //{key: "V", mods: "", desc: "Save video into playlist", func: a => eleClick(':is(#info, #description-and-actions) #menu ytd-button-renderer:last-of-type button#button,#actions button:has(div[style*="list_add"])')},
    //{key: "U", mods: "", desc: "Toggle subscription", func: a => eleClick('#meta ytd-subscribe-button-renderer>.ytd-subscribe-button-renderer:not(div),paper-button.ytd-subscribe-button-renderer,tp-yt-paper-button.ytd-subscribe-button-renderer,ytd-subscribe-button-renderer.ytd-watch-metadata button', true)},
    //{key: "Y", mods: "", desc: "Toggle subscription notification", func: a => eleClick(['ytd-watch-flexy #meta  .ytd-subscription-notification-toggle-button-renderer>button#button', 'ytd-watch-flexy #notification-preference-toggle-button > .ytd-subscribe-button-renderer'])},
    //{key: "R", mods: "", desc: "Toggle replay chat or chapter list", func: toggleChatChap},
    //{key: "R", mods: "S", desc: "Toggle sponsored video list", func: a => eleClick('ytd-engagement-panel-section-list-renderer[target-id="ytbc-related-shelf"] #visibility-button .yt-icon-button')},
    //{key: "X", mods: "", desc: "Toggle autoplay of next non-playlist video", func: a => eleClick(['paper-toggle-button.ytd-compact-autoplay-renderer', 'button[data-tooltip-target-id="ytp-autonav-toggle-button"]'])},
    //{key: "Y", mods: "S", desc: "Go to user/channel playlists page", func: a => navUser("Playlists", "playlists")},

    //{key: "S", mods: "S", desc: 'Go to Subscriptions page', func: a => eleClick('a[href="/feed/subscriptions"]') || (location.href = "/feed/subscriptions")},
    //{key: "I", mods: "S", desc: "Go to History page", func: a => eleClick('a[href="/feed/history"]') || (location.href = "/feed/history")},
    //{key: "W", mods: "S", desc: "Go to Watch Later page", func: a => eleClick('a[href="/playlist?list=WL"]') || (location.href = "/playlist?list=WL")},
    //{key: "K", mods: "S", desc: "Go to Liked Videos page", func: a => eleClick('a[href="/playlist?list=LL"]') || (location.href = "/playlist?list=LL")},
    //{key: "T", mods: "S", desc: "Go to Account page", func: a => eleClick('a[href="/account"]') || (location.href = "/account")}
  ];
  var subtitleLanguageCode = "zh"; //2-letters language code for select preferred subtitle language hotkey

  //=== CONFIGURATION END

  var baseKeys = {};
  ("~`!1@2#3$4%5^6&7*8(9)0_-+={[}]:;\"'|\\<,>.?/").split("").forEach((c, i, a) => {
    if ((i & 1) === 0) baseKeys[c] = a[i + 1];
  });

  function isHidden(e) {
    while (e && e.style) {
      if (getComputedStyle(e).display === "none") {
        return true;
      }
      e = e.parentNode
    }
    return false
  }

  function eleClick(s, l, e) {
    if (s.some) {
      s.some(a => {
        if (e = document.querySelector(a)) {
          if (e.disabled || isHidden(e)) e = null
        }
        if (e) return true
      });
    } else if (l) {
      e = Array.from(document.querySelectorAll(s)).find(f => !f.disabled && !isHidden(f))
    } else if (e = document.querySelector(s)) {
      if (e.disabled || isHidden(e)) e = null
    }
    if (e) {
      e.click();
      return true
    }
  }

  function videoSeekBy(t, v) {
    (v = document.querySelector('.html5-video-player')) && v.seekBy(t);
  }

  function videoSeekTo(p, v) {
    (v = document.querySelector('.html5-video-player')) && v.seekTo(v.getDuration() * p);
  }

  function videoSeekChapter(d, v, s, t) {
    if (
      (v = document.querySelector('.html5-video-player')) && (s = v.getPlayerResponse().videoDetails) &&
      (s = s.shortDescription)
    ) {
      t = v.getCurrentTime();
      if (s = s.match(/^(?:\s*\d+\.)?\s*(\d{1,2}:)?\d{1,2}:\d{1,2}\s+\S+.*/gm)) {
        s = s.map(s => {
          s = s.match(/^(?:\s*\d+\.)?\s*(\d{1,2}:)?(\d{1,2}):(\d{1,2})/);
          s[1] = s[1] ? parseInt(s[1]) : 0;
          s[2] = s[2] ? parseInt(s[2]) : 0;
          s[3] = s[3] ? parseInt(s[3]) : 0;
          return (s[1] * 3600) + (s[2] * 60) + s[3]
        })
      }
    }
    if (
      (!s || !s.some || !s.length) && (s = window["page-manager"]) && (s = s.getCurrentData()) && (s = s.response) && (s = s.playerOverlays) &&
      (s = s.playerOverlayRenderer) && (s = s.decoratedPlayerBarRenderer) && (s = s.decoratedPlayerBarRenderer) && (s = s.playerBar) &&
      (s = s.multiMarkersPlayerBarRenderer) && (s = s.markersMap)
    ) {
      s.some(m => {
        if (m.key === "AUTO_CHAPTERS") {
          if ((m = m.value) && (m = m.chapters) && m.length && m[0] && m[0].chapterRenderer && m[0].chapterRenderer) {
            s = m.map(a => Math.floor(a.chapterRenderer.timeRangeStartMillis / 1000))
          }
          return true
        }
      })
    }
    if (s && s.some) {
      if (s.length && (s[0] > 1)) s.unshift(0);
      s.some((c, i) => {
        if ((d < 0) && (c <= t) && (!s[i + 1] || (s[i + 1] > t))) {
          if ((c + 1) >= t) {
            v.seekTo(s[i - 1]);
          } else v.seekTo(c);
          return true
        } else if ((d > 0) && (c > t) && i) {
          v.seekTo(c);
          return true
        }
      })
    }
  }

  function selectQuality(i, v, e, c) {
    if ((v = document.querySelector('.html5-video-player')) && (v.getAvailableQualityLabels().length > 1)) {
      if (i.key === "Y") {
        v.setPlaybackQualityRange("auto", "auto");
      } else {
        (e = v.getAvailableQualityLevels()).pop();
        c = e.indexOf(v.getPlaybackQuality());
        i = i.key === "G" ? 1 : -1;
        if (e = e[c + i]) v.setPlaybackQualityRange(e, e);
      }
    }
  }

  function adjustSpeed(d, s) {
    if (s = Math.floor((movie_player.getPlaybackRate() * 4) + d) / 4) movie_player.setPlaybackRate(s)
          showNotification('Playback rate : ' + movie_player.getPlaybackRate(s))
  }
function adjustSpeed2(d, s) {
    if (s = 1) movie_player.setPlaybackRate(s)
      showNotification(movie_player.getPlaybackRate(s))
  }
  function selectCaption(v, o, c, a) {
    if (
      (v = document.querySelector('.html5-video-player')) && (o = v.getPlayerResponse().captions) &&
      (o = o.playerCaptionsTracklistRenderer) && (o = o.captionTracks)
    ) {
      if ((c = v.getOption("captions", "track")) && c.vss_id) {
        if (c.vss_id === ("." + subtitleLanguageCode)) {
          a = o.find(ct => ct.vssId === ("a." + subtitleLanguageCode));
          if (!a) a = o.find(ct => ct.isTranslatable && (ct.vssId[0] === ".") && (ct.vssId.substr(1) !== subtitleLanguageCode));
          if (!a) a = o.find(ct => ct.isTranslatable && (ct.vssId[1] === ".") && (ct.vssId.substr(2) !== subtitleLanguageCode));
        }
        if (!a && (c.vss_id === ("a." + subtitleLanguageCode))) {
          a = o.find(ct => ct.isTranslatable && (ct.vssId[0] === ".") && (ct.vssId.substr(1) !== subtitleLanguageCode));
          if (!a) a = o.find(ct => ct.isTranslatable && (ct.vssId[1] === ".") && (ct.vssId.substr(2) !== subtitleLanguageCode));
        }
        if (!a && c.is_translateable && (c.vss_id[0] === ".") && (c.vss_id.substr(1) !== subtitleLanguageCode)) {
          a = o.find(ct => ct.isTranslatable && (ct.vssId[1] === ".") && (ct.vssId.substr(2) !== subtitleLanguageCode));
        }
      }
      if (!a) {
        a = o.find(ct => ct.vssId === ("." + subtitleLanguageCode));
        if (!a) a = o.find(ct => ct.vssId === ("a." + subtitleLanguageCode));
        if (!a) a = o.find(ct => ct.isTranslatable && (ct.vssId[0] === ".") && (ct.vssId.substr(1) !== subtitleLanguageCode));
        if (!a) a = o.find(ct => ct.isTranslatable && (ct.vssId[1] === ".") && (ct.vssId.substr(2) !== subtitleLanguageCode));
        if (!a) {
          a = o.find(ct => ct.isTranslatable && (
            ((ct.vssId[0] === ".") && (ct.vssId.substr(1) !== subtitleLanguageCode)) ||
            ((ct.vssId[1] === ".") && (ct.vssId.substr(2) !== subtitleLanguageCode))
          ));
        }
        if (!a) return;
      }
      a = {languageCode: a.languageCode, vss_id: a.vssId};
      if (a.languageCode !== subtitleLanguageCode) {
        v.getPlayerResponse().captions.playerCaptionsTracklistRenderer.translationLanguages.some(l => {
          if (l.languageCode === subtitleLanguageCode) {
            a.translationLanguage = {languageCode: subtitleLanguageCode};
            a.translationLanguage.languageName = l.languageName.simpleText;
            return true;
          }
        });
      }
      if (!c.languageCode) v.toggleSubtitles();
      v.setOption("captions", "track", a);
    }
  }

  function toggleChatChap(a) {
    if (!eleClick([
      '#chat-messages #close-button button',
      '#show-hide-button.ytd-live-chat-frame button',
      '#show-hide-button.ytd-live-chat-frame > ytd-toggle-button-renderer.ytd-live-chat-frame',
      'ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-macro-markers-description-chapters"][visibility="ENGAGEMENT_PANEL_VISIBILITY_EXPANDED"] #visibility-button button',
      '.ytp-chapter-title'
    ]) && (a = document.querySelector('ytd-live-chat-frame:not([collapsed]) #chatframe'))) a.contentWindow.postMessage("myhujs_toggleChatChap")
  }

  function toggleYtVideoControls(v) {
    if (v = document.querySelector('.html5-video-player')) {
      if (v.classList.contains("ytp-autohide-active")) {
        v.classList.remove("ytp-autohide-active")
      } else if (v.classList.contains("ytp-autohide")) {
        v.classList.remove("ytp-autohide")
      } else v.classList.add("ytp-autohide")
    }
  }

  function navUser(tn, tp, a, b, d) {
    if ((new RegExp(`^/(channel|user)/[^/]+/${tp}$`)).test(location.pathname)) {
      Array.from(document.querySelectorAll('.paper-tab')).some(e => {
        if (e.textContent.trim() === tn) {
          e.parentNode.click();
          return true;
        }
      });
    } else if (
      (a = document.querySelector(':is(.ytd-video-secondary-info-renderer,ytd-watch-metadata) yt-formatted-string.ytd-channel-name')) &&
      (d = a.__data) && (d = d.text) && (d = d.runs) && (d = d[0]) && (d = d.navigationEndpoint)
    ) {
      if (b = document.querySelector(".yt-page-navigation-progress")) {
        b.style.transform = "scaleX(.5)";
        b.parentNode.hidden = false
      }
      fetch(d.commandMetadata.webCommandMetadata.url, {credentials: "omit"}).then(r => r.text().then((h, x, ep, e, t, m) => {
        if ((h = h.match(/var ytInitialData = (\{.*?\});/)) && (h = JSON.parse(h[1]).contents.twoColumnBrowseResultsRenderer.tabs)) {
          x = new RegExp(`^\\/[^\\/]+(?:\\/[^\\/]+)?\\/${tp}$`);
          if (h.some((v, i, b) => {
            if ((b = v.tabRenderer) && !b.content && x.test((b = b.endpoint).commandMetadata.webCommandMetadata.url)) {
              e = (ep = d).browseEndpoint;
              t = d.clickTrackingParams;
              m = d.commandMetadata;
              d.browseEndpoint = b.browseEndpoint;
              d.clickTrackingParams = b.clickTrackingParams;
              d.commandMetadata = b.commandMetadata;
              return true
            }
          })) {
            a.firstElementChild.click();
            setTimeout(() => {
              ep.browseEndpoint = e;
              ep.clickTrackingParams = t;
              ep.commandMetadata = m;
            }, 20)
          }
        }
      }))
    }
  }

  function checkHotkeyPopup(a, b, c, d, e) {
    if ((a = document.querySelector("#sections.ytd-hotkey-dialog-content")) && !a.querySelector(".more-hotkeys")) {
      a.__shady_native_appendChild(b = (d = a.firstElementChild).__shady_native_cloneNode(false)).classList.add("more-hotkeys");
      a.__shady_native_appendChild(d.__shady_native_cloneNode(false));
      b.__shady_native_appendChild(d.__shady_native_firstElementChild.__shady_native_cloneNode(false)).textContent = "More Hotkeys";
      c = b.__shady_native_appendChild(d.__shady_native_lastElementChild.__shady_native_cloneNode(false));
      d = d.__shady_native_lastElementChild.firstElementChild;
      hotkeys.forEach((h, e, f) => {
        if (h.desc) {
          e = c.__shady_native_appendChild(d.__shady_native_cloneNode(true));
          e.__shady_native_firstElementChild.textContent = h.desc;
          if (!(f = h.keys)) {
            if (h.ctrl || h.alt) {
              f = (h.ctrl ? "CTRL+" : "") + (h.shift ? "SHIFT+" : "") + (h.alt ? "ALT+" : "") + h.key;
            } else if (h.shift) {
              f = h.key + " (" + (h.shift ? "SHIFT+" : "") + (h.shift ? baseKeys[h.key] || h.key.toLowerCase() : h.key) + ")";
            } else f = h.key.toLowerCase();
          }
          e.__shady_native_lastElementChild.textContent = f;
        }
      });
    } else if (--ch) setTimeout(checkHotkeyPopup, 100);
  }

  function editable(e) {
    var r = false;
    while (e) {
      if (e.contentEditable === "true") return true;
      e = e.parentNode;
    }
    return r;
  }

  if (top !== self) {
    addEventListener("message", ev => (ev.data === "myhujs_toggleChatChap") && toggleChatChap())
  }

  hotkeys.forEach(h => {
    var a = h.mods.toUpperCase().split("");
    h.shift = a.includes("S");
    h.ctrl = a.includes("C");
    h.alt = a.includes("A");
  });
  addEventListener("keydown", (ev, a) => {
    if ((a = document.activeElement) && (editable(a) || (a.tagName === "INPUT") || (a.tagName === "TEXTAREA"))) return;
    if ((ev.key === "?") && ev.shiftKey && !ev.ctrlKey && !ev.altKey) {
      ch = 10;
      setTimeout(checkHotkeyPopup, 100);
    }
    hotkeys.forEach(h => {
      if ((ev.key.toUpperCase() === h.key) && (ev.shiftKey === h.shift) && (ev.ctrlKey === h.ctrl) && (ev.altKey === h.alt)) {
        ev.preventDefault();
        ("function" === typeof h.func) && h.func(h);
      }
    });
  }, true);

})();