🏠 Home 

YT快速鍵hotkeys

YT快速鍵控制功能,說明鍵:[?]


安装此脚本?
  1. // ==UserScript==
  2. // @name YT快速鍵hotkeys
  3. // @namespace https://greasyfork.org/users/4839
  4. // @version 1.3.0
  5. // @license AGPLv3
  6. // @author jcunews
  7. // @description YT快速鍵控制功能,說明鍵:[?]
  8. // @match https://*.youtube.com/*
  9. // @grant none
  10. // @run-at document-start
  11. // @icon https://www.youtube.com/favicon.ico
  12. // ==/UserScript==
  13. //感謝原作者https://greasyfork.org/zh-TW/scripts/383570
  14. /*
  15. *[?](shift+/)打開說明欄
  16. *快速鍵可代碼內自行修改
  17. *本腳本無法突破YT限制(1.速度上限2x;2.LIVE直播若關閉回放則無法跳秒)
  18. *因此可同時使用html5腳本
  19. https://greasyfork.org/zh-TW/scripts/487825
  20. */
  21. //速度提示框
  22. const SHOW_NOTIFICATIONS = true;
  23. const NOTIFICATION_DURATION_MILLIS = 350;
  24. var lastToastElement = null;
  25. function showNotification(message) {
  26. if (!SHOW_NOTIFICATIONS) {return;}
  27. if (lastToastElement !== null) {lastToastElement.remove();lastToastElement = null;}
  28. const toast = document.createElement('tp-yt-paper-toast');
  29. toast.innerText = message;
  30. toast.classList.add('toast-open');
  31. toast.style.cssText= "font:normal 16pt/normal sans-serif;background:#444;color:#fff";
  32. const styleProps = {position: 'fixed',left: '0%',bottom: '2%',opacity: '1',};//outline: 'none',zIndex: '2202',
  33. for (const prop in styleProps) {toast.style[prop] = styleProps[prop];}
  34. document.body.appendChild(toast);
  35. lastToastElement = toast;
  36. setTimeout(() => {toast.style.display = 'block'; toast.style.transform = 'none';}, 0);
  37. setTimeout(() => {toast.style.display = 'none';}, Math.max(0, NOTIFICATION_DURATION_MILLIS));
  38. }
  39. (ch => {
  40. /*
  41. `key` 必須輸入大寫。
  42. `mods`是零或最多3個修飾符鍵以任何順序): `A`=Alt, `C`=Control, `S`=Shift.
  43. 例如 “”(無修飾符鍵), "s" (Shift), "Cs" (Control+Shift), "aSc" (Control+Shift+Alt).
  44. */
  45. var hotkeys = [
  46. {key: "S", mods: "", desc: "截圖(jpg)Screenshot >腳本https://greasyfork.org/scripts/488800", func: a => eleClick('#yt-ss-btn')},
  47. //{key: "S", mods: "", desc: "截圖到剪貼簿(停頓)clipboard Screenshot >腳本https://greasyfork.org/scripts/488885", func: a => eleClick('#ytp-screenshot-button')},
  48. {key: "S", mods: "", desc: "截圖到剪貼簿clipboard Screenshot >腳本https://greasyfork.org/scripts/512256", func: a => eleClick('#screenshotButton')},
  49. //{key: "S", mods: "", desc: "截圖腳本(png)TesterTV Screenshots >搭配腳本https://greasyfork.org/scripts/482417", func: a => eleClick('#ScreenshotButton')},
  50. {key: "W", mods: "", desc: "Tabview資訊 >搭配腳本https://greasyfork.org/scripts/428651", func: a => eleClick('#tab-btn1')},
  51. {key: "E", mods: "", desc: "Tabview留言 >搭配腳本https://greasyfork.org/scripts/428651", func: a => eleClick('#tab-btn3')},
  52. {key: "W", mods: "S", desc: "Tabview播放清單 >搭配腳本https://greasyfork.org/scripts/428651", func: a => eleClick('#tab-btn5')},
  53. {key: "E", mods: "S", desc: "Tabview影片 >搭配腳本https://greasyfork.org/scripts/428651", func: a => eleClick('#tab-btn4')},
  54. {key: "R", mods: "S", desc: "已儲存進度 >搭配腳本https://greasyfork.org/scripts/487305", func: a => eleClick('.ysrp-settings-button')},
  55. {key: "ESCAPE", mods: "", desc: "[關閉YouTubeLiveClock筆記] >搭配擴充YouTubeLiveClock", func: a => eleClick('#ytlctn-close')},
  56. {key: "/", mods: "", desc: "焦點頻道搜尋框", func: a => eleClick('#tabs-container :is(ytd-expandable-tab-renderer,.ytd-expandable-tab-renderer):has(form[action*="/search"]) button.yt-icon-button')},
  57. {key: "'", mods: "", desc: "焦點評論輸入框", func: a => eleClick('#simplebox-placeholder')},
  58. {key: "/",mods: "", desc: "焦點聊天室輸入框", func: a => focusChatFrame('div#input')},
  59. {key: "N", mods: "", desc: "聊天室跳到最底↓", func: a => eleClick('#show-more')},
  60. {key: "B",mods: "", desc: "焦點即時聊天訊息列表+刷新", func: a => (focusChatFrame('tp-yt-paper-button') ,eleClick('tp-yt-paper-button'), eleClick('.iron-selected.yt-dropdown-menu'))},
  61. //{key: "\\", mods: "", desc: "聊天室輸入框Focus live chat input box", func: (a, b) => (a = document.querySelector('#chatframe')) && (b = a.contentDocument.querySelector('div#input')) && (a.focus(), b.focus())},
  62. //{key: "B", mods: "", desc: "Focus聚焦聊天室", func: a => (a = document.querySelector('#chatframe')) && (a.focus())},
  63. //{key: "B", mods: "", desc: "聊天室更新1+2", func: a => eleClick('.iron-selected.yt-dropdown-menu')},
  64. //{key: "B", mods: "", desc: "聊天室下拉按鈕3", func: a => eleClick('tp-yt-paper-button')},
  65. //{key: "B", mods: "", desc: "聊天室更新3", func: a => eleClick('tp-yt-paper-item')},
  66. {key: "G", mods: "", desc: "倒退 1 秒Rewind video by 1 seconds", func: a => videoSeekBy(-1)},
  67. {key: "H", mods: "", desc: "前進 1 秒forward video by 1 seconds", func: a => videoSeekBy(1)},
  68. {key: "G", mods: "S", desc: "前進 30 秒Rewind video by 30 seconds", func: a => videoSeekBy(-30)},
  69. {key: "H", mods: "S", desc: "倒退 30 秒forward video by 30 seconds", func: a => videoSeekBy(30)},
  70. {key: "G", mods: "SA", desc: "跳至開始go video 0%", func: a => videoSeekTo(0.00)},
  71. {key: "H", mods: "SA", desc: "跳至最後go video 100%", func: a => videoSeekTo(1.00)},
  72. {key: "F", mods: "S", desc: "跳至直播最新進度live current time", func: a => eleClick('.ytp-live-badge')},
  73. {key: "J", mods: "A", func: a => videoSeekChapter(-1), desc: "上一章節Seek to previous chapter"},
  74. {key: "L", mods: "A", func: a => videoSeekChapter(1), desc: "下一章節Seek to next chapter"},
  75. {key: "`", mods: "", desc: "指南/側邊欄Toggle guide / sidebar", func: a => eleClick('#guide-button')},
  76. {key: "V", mods: "S", desc: "隱藏控制列Toggle YouTube video controls", func: toggleYtVideoControls},
  77. {key: "B", mods: "S", desc: "個人中心Go to feed you page", func: a => (window.open("/feed/you"))},
  78. {key: "R", mods: "S", desc: "首頁Go to homepage", func: a => (window.open("/"))},
  79. {key: "R", mods: "", desc: "聊天室/章節Toggle replay chat or chapter list", func: toggleChatChap},
  80. {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'])},
  81. //{key: "~", mods: "S", desc: "首頁Go to YouTube home page", func: a => eleClick('a#logo')},
  82. {key: "G", mods: "A", desc: "提高解析度Decrease video quality", func: selectQuality},
  83. {key: "H", mods: "A", desc: "降低解析度Increase video quality", func: selectQuality},
  84. //{key: "Y", mods: "A", desc: "自動解析度Set video quality to auto", func: selectQuality},
  85. {key: "<", mods: "S", desc: "加速(上限 2X )Decrease video playback speed by 0.25", func: a => adjustSpeed(-1)},
  86. {key: ">", mods: "S", desc: "減速(下限0.25X)Increase video playback speed by 0.25", func: a => adjustSpeed(1)},
  87. {key: "M", mods: "S", desc: "恢復原速Increase video playback speed by 1", func: a => adjustSpeed2()},
  88. //禁用0-9的跳進度%
  89. {key: "0", mods: "",desc: "說明:已經禁用 0 - 9 跳進度 N %", func: a => videoSeekTo(none)},
  90. {key: "1", mods: "", func: a => videoSeekTo(none)},
  91. {key: "2", mods: "", func: a => videoSeekTo(none)},
  92. {key: "3", mods: "", func: a => videoSeekTo(none)},
  93. {key: "4", mods: "", func: a => videoSeekTo(none)},
  94. {key: "5", mods: "", func: a => videoSeekTo(none)},
  95. {key: "6", mods: "", func: a => videoSeekTo(none)},
  96. {key: "7", mods: "", func: a => videoSeekTo(none)},
  97. {key: "8", mods: "", func: a => videoSeekTo(none)},
  98. {key: "9", mods: "", func: a => videoSeekTo(none)},
  99. ];
  100. var baseKeys = {};
  101. ("~`!1@2#3$4%5^6&7*8(9)0_-+={[}]:;\"'|\\<,>.?/").split("").forEach((c, i, a) => {
  102. if ((i & 1) === 0) baseKeys[c] = a[i + 1];
  103. });
  104. function focusChatFrame(sel, a, b) {
  105. (a = document.querySelector('#chatframe')) && (b = a.contentDocument.querySelector(sel)) && (a.focus(), b.focus())
  106. }
  107. function isHidden(e) {
  108. while (e && e.style) {
  109. if (getComputedStyle(e).display === "none") {
  110. return true;
  111. }
  112. e = e.parentNode
  113. }
  114. return false
  115. }
  116. function eleClick(s, l, e) {
  117. if (s.some) {
  118. s.some(a => {
  119. if (e = document.querySelector(a)) {
  120. if (e.disabled || isHidden(e)) e = null
  121. }
  122. if (e) return true
  123. });
  124. } else if (l) {
  125. e = Array.from(document.querySelectorAll(s)).find(f => !f.disabled && !isHidden(f))
  126. } else if (e = document.querySelector(s)) {
  127. if (e.disabled || isHidden(e)) e = null
  128. }
  129. if (e) {
  130. e.click();
  131. return true
  132. }
  133. }
  134. function videoSeekBy(t, v) {
  135. (v = document.querySelector('.html5-video-player')) && v.seekBy(t);
  136. }
  137. function videoSeekTo(p, v) {
  138. (v = document.querySelector('.html5-video-player')) && v.seekTo(v.getDuration() * p);
  139. }
  140. function videoSeekChapter(d, v, s, t) {
  141. if (
  142. (v = document.querySelector('.html5-video-player')) && (s = v.getPlayerResponse().videoDetails) &&
  143. (s = s.shortDescription)
  144. ) {
  145. t = v.getCurrentTime();
  146. if (s = s.match(/^(?:\s*\d+\.)?\s*(\d{1,2}:)?\d{1,2}:\d{1,2}\s+\S+.*/gm)) {
  147. s = s.map(s => {
  148. s = s.match(/^(?:\s*\d+\.)?\s*(\d{1,2}:)?(\d{1,2}):(\d{1,2})/);
  149. s[1] = s[1] ? parseInt(s[1]) : 0;
  150. s[2] = s[2] ? parseInt(s[2]) : 0;
  151. s[3] = s[3] ? parseInt(s[3]) : 0;
  152. return (s[1] * 3600) + (s[2] * 60) + s[3]
  153. })
  154. }
  155. }
  156. if (
  157. (!s || !s.some || !s.length) && (s = window["page-manager"]) && (s = s.getCurrentData()) && (s = s.response) && (s = s.playerOverlays) &&
  158. (s = s.playerOverlayRenderer) && (s = s.decoratedPlayerBarRenderer) && (s = s.decoratedPlayerBarRenderer) && (s = s.playerBar) &&
  159. (s = s.multiMarkersPlayerBarRenderer) && (s = s.markersMap)
  160. ) {
  161. s.some(m => {
  162. if (m.key === "AUTO_CHAPTERS") {
  163. if ((m = m.value) && (m = m.chapters) && m.length && m[0] && m[0].chapterRenderer && m[0].chapterRenderer) {
  164. s = m.map(a => Math.floor(a.chapterRenderer.timeRangeStartMillis / 1000))
  165. }
  166. return true
  167. }
  168. })
  169. }
  170. if (s && s.some) {
  171. if (s.length && (s[0] > 1)) s.unshift(0);
  172. s.some((c, i) => {
  173. if ((d < 0) && (c <= t) && (!s[i + 1] || (s[i + 1] > t))) {
  174. if ((c + 1) >= t) {
  175. v.seekTo(s[i - 1]);
  176. } else v.seekTo(c);
  177. return true
  178. } else if ((d > 0) && (c > t) && i) {
  179. v.seekTo(c);
  180. return true
  181. }
  182. })
  183. }
  184. }
  185. //解析度修改
  186. function selectQuality(i, v, e, c) {
  187. if ((v = document.querySelector('.html5-video-player')) && (v.getAvailableQualityLabels().length > 1)) {
  188. if (i.key === "Y") {
  189. v.setPlaybackQualityRange("auto", "auto");
  190. } else {
  191. (e = v.getAvailableQualityLevels()).pop();
  192. c = e.indexOf(v.getPlaybackQuality());
  193. i = i.key === "G" ? 1 : -1;
  194. if (e = e[c + i]) v.setPlaybackQualityRange(e, e);
  195. }
  196. }
  197. }
  198. //速度修改
  199. function adjustSpeed(d, s) {
  200. if (s = Math.floor((movie_player.getPlaybackRate() * 4) + d) / 4) movie_player.setPlaybackRate(s)
  201. showNotification('Speed : ' + movie_player.getPlaybackRate(s) + ' X')
  202. }
  203. //速度復原1x
  204. function adjustSpeed2(d, s) {
  205. if (s = 1) movie_player.setPlaybackRate(s)
  206. showNotification('Speed : ' + movie_player.getPlaybackRate(s) + ' X')
  207. }
  208. //顯示隱藏聊天室/章節按鈕
  209. function toggleChatChap(a) {
  210. if (!eleClick([
  211. '#chat-messages #close-button button',
  212. '#show-hide-button.ytd-live-chat-frame button',
  213. '#show-hide-button.ytd-live-chat-frame > ytd-toggle-button-renderer.ytd-live-chat-frame',
  214. 'ytd-engagement-panel-section-list-renderer:is([target-id="engagement-panel-macro-markers-auto-chapters"],[target-id="engagement-panel-macro-markers-description-chapters"])[visibility="ENGAGEMENT_PANEL_VISIBILITY_EXPANDED"] #visibility-button button',
  215. '.ytp-chapter-title'
  216. ]) && (a = document.querySelector('ytd-live-chat-frame:not([collapsed]) #chatframe'))) a.contentWindow.postMessage("myhujs_toggleChatChap")
  217. }
  218. //影片控制列隱藏顯示
  219. function toggleYtVideoControls(v) {
  220. if (v = document.querySelector('.html5-video-player')) {
  221. if (v.classList.contains("ytp-autohide-active")) {
  222. v.classList.remove("ytp-autohide-active")
  223. } else if (v.classList.contains("ytp-autohide")) {
  224. v.classList.remove("ytp-autohide")
  225. } else v.classList.add("ytp-autohide")
  226. }
  227. }
  228. function navUser(tn, tp, a, b, d) {
  229. if ((new RegExp(`^/(channel|user)/[^/]+/${tp}$`)).test(location.pathname)) {
  230. Array.from(document.querySelectorAll('.paper-tab')).some(e => {
  231. if (e.textContent.trim() === tn) {
  232. e.parentNode.click();
  233. return true;
  234. }
  235. });
  236. } else if (
  237. (a = document.querySelector(':is(.ytd-video-secondary-info-renderer,ytd-watch-metadata) yt-formatted-string.ytd-channel-name')) &&
  238. (d = a.__data) && (d = d.text) && (d = d.runs) && (d = d[0]) && (d = d.navigationEndpoint)
  239. ) {
  240. if (b = document.querySelector(".yt-page-navigation-progress")) {
  241. b.style.transform = "scaleX(.5)";
  242. b.parentNode.hidden = false
  243. }
  244. fetch(d.commandMetadata.webCommandMetadata.url, {credentials: "omit"}).then(r => r.text().then((h, x, ep, e, t, m) => {
  245. if ((h = h.match(/var ytInitialData = (\{.*?\});/)) && (h = JSON.parse(h[1]).contents.twoColumnBrowseR###ltsRenderer.tabs)) {
  246. x = new RegExp(`^\\/[^\\/]+(?:\\/[^\\/]+)?\\/${tp}$`);
  247. if (h.some((v, i, b) => {
  248. if ((b = v.tabRenderer) && !b.content && x.test((b = b.endpoint).commandMetadata.webCommandMetadata.url)) {
  249. e = (ep = d).browseEndpoint;
  250. t = d.clickTrackingParams;
  251. m = d.commandMetadata;
  252. d.browseEndpoint = b.browseEndpoint;
  253. d.clickTrackingParams = b.clickTrackingParams;
  254. d.commandMetadata = b.commandMetadata;
  255. return true
  256. }
  257. })) {
  258. a.firstElementChild.click();
  259. setTimeout(() => {
  260. ep.browseEndpoint = e;
  261. ep.clickTrackingParams = t;
  262. ep.commandMetadata = m;
  263. }, 20)
  264. }
  265. }
  266. }))
  267. }
  268. }
  269. function checkHotkeyPopup(a, b, c, d, e) {
  270. if ((a = document.querySelector("#sections.ytd-hotkey-dialog-content")) && !a.querySelector(".more-hotkeys")) {
  271. a.__shady_native_appendChild(b = (d = a.firstElementChild).__shady_native_cloneNode(false)).classList.add("more-hotkeys");
  272. a.__shady_native_appendChild(d.__shady_native_cloneNode(false));
  273. b.__shady_native_appendChild(d.__shady_native_firstElementChild.__shady_native_cloneNode(false)).textContent = "More Hotkeys";
  274. c = b.__shady_native_appendChild(d.__shady_native_lastElementChild.__shady_native_cloneNode(false));
  275. d = d.__shady_native_lastElementChild.firstElementChild;
  276. hotkeys.forEach((h, e, f) => {
  277. if (h.desc) {
  278. e = c.__shady_native_appendChild(d.__shady_native_cloneNode(true));
  279. e.__shady_native_firstElementChild.textContent = h.desc;
  280. if (!(f = h.keys)) {
  281. if (h.ctrl || h.alt) {
  282. f = (h.ctrl ? "CTRL+" : "") + (h.shift ? "SHIFT+" : "") + (h.alt ? "ALT+" : "") + h.key;
  283. } else if (h.shift) {
  284. f = h.key + " (" + (h.shift ? "SHIFT+" : "") + (h.shift ? baseKeys[h.key] || h.key.toLowerCase() : h.key) + ")";
  285. } else f = h.key.toLowerCase();
  286. }
  287. e.__shady_native_lastElementChild.textContent = f;
  288. }
  289. });
  290. } else if (--ch) setTimeout(checkHotkeyPopup, 100);
  291. }
  292. function editable(e) {
  293. var r = false;
  294. while (e) {
  295. if (e.contentEditable === "true") return true;
  296. e = e.parentNode;
  297. }
  298. return r;
  299. }
  300. if (top !== self) {
  301. addEventListener("message", ev => (ev.data === "myhujs_toggleChatChap") && toggleChatChap())
  302. }
  303. hotkeys.forEach(h => {
  304. var a = h.mods.toUpperCase().split("");
  305. h.shift = a.includes("S");
  306. h.ctrl = a.includes("C");
  307. h.alt = a.includes("A");
  308. });
  309. addEventListener("keydown", (ev, a) => {
  310. if ((a = document.activeElement) && (editable(a) || (a.tagName === "INPUT") || (a.tagName === "TEXTAREA"))) return;
  311. if ((ev.key === "?") && ev.shiftKey && !ev.ctrlKey && !ev.altKey) {
  312. ch = 10;
  313. setTimeout(checkHotkeyPopup, 100);
  314. }
  315. hotkeys.forEach(h => {
  316. if ((ev.key.toUpperCase() === h.key) && (ev.shiftKey === h.shift) && (ev.ctrlKey === h.ctrl) && (ev.altKey === h.alt)) {
  317. ev.preventDefault();
  318. ("function" === typeof h.func) && h.func(h);
  319. }
  320. });
  321. }, true);
  322. })();