Youtube Mobile Enhance 油管移动端增强

针对油管移动端,点击视频新标签页打开,记忆播放速度,突破播放速度限制

  1. // ==UserScript==
  2. // @name Youtube Mobile Enhance 油管移动端增强
  3. // @namespace http://tampermonkey.net/
  4. // @version 2.9.2
  5. // @author zyronon
  6. // @description 针对油管移动端,点击视频新标签页打开,记忆播放速度,突破播放速度限制
  7. // @license GPL License
  8. // @icon https://v2next.netlify.app/favicon.ico
  9. // @homepage https://github.com/zyronon/web-scripts
  10. // @homepageURL https://github.com/zyronon/web-scripts
  11. // @supportURL https://update.gf.qytechs.cn/scripts/487013/Youtube%20Mobile%20Enhance%20%E6%B2%B9%E7%AE%A1%E7%A7%BB%E5%8A%A8%E7%AB%AF%E5%A2%9E%E5%BC%BA.user.js
  12. // @match https://m.youtube.com/*
  13. // @require https://cdn.jsdelivr.net/npm/vue@3.4.38/dist/vue.global.prod.js
  14. // @require https://cdn.jsdelivr.net/npm/eruda@3.2.3/eruda.js
  15. // @grant GM_addStyle
  16. // @grant GM_openInTab
  17. // @grant unsafeWindow
  18. // @run-at document-start
  19. // ==/UserScript==
  20.  
  21. (t=>{if(typeof GM_addStyle=="function"){GM_addStyle(t);return}const e=document.createElement("style");e.textContent=t,document.head.append(e)})(" html{font-size:12px!important}.ytb-next{font-size:1.4rem;display:flex;position:fixed;top:0;right:10px;width:calc(22vw - 10px);z-index:99999;background:black}.ytb-next .btn{flex:1;color:#f1f1f1;background-color:#ffffff1a;padding:5px 0;height:36px;font-size:14px;line-height:36px;text-align:center;border:1px solid rgba(0,0,0,.8)}.msg{position:fixed;z-index:999;font-size:3rem;left:0;top:0;color:#000;background:white;padding:1rem 2rem}@media (min-width: 1280px) and (orientation: landscape){.player-container,.player-container.sticky-player{right:22vw!important;top:0!important;z-index:999!important}ytm-watch{margin-right:22vw!important}ytm-engagement-panel,.related-items-container{width:22vw!important}lazy-list .feed-item{width:100%!important}lazy-list .feed-item ytm-media-item{width:100%!important}.playlist-entrypoint-background-protection,.slide-in-animation-entry-point{width:22vw!important}ytm-single-column-watch-next-results-renderer [section-identifier=related-items],ytm-single-column-watch-next-results-renderer>ytm-playlist{width:22vw!important;padding:0 0 8px 8px;box-sizing:border-box}ytm-single-column-watch-next-results-renderer .playlist-content{width:22vw!important}} ");
  22.  
  23. (function (vue, eruda) {
  24. 'use strict';
  25.  
  26. function _interopNamespaceDefault(e) {
  27. const n = Object.create(null, { [Symbol.toStringTag]: { value: 'Module' } });
  28. if (e) {
  29. for (const k in e) {
  30. if (k !== 'default') {
  31. const d = Object.getOwnPropertyDescriptor(e, k);
  32. Object.defineProperty(n, k, d.get ? d : {
  33. enumerable: true,
  34. get: () => e[k]
  35. });
  36. }
  37. }
  38. }
  39. n.default = e;
  40. return Object.freeze(n);
  41. }
  42.  
  43. const eruda__namespace = /*#__PURE__*/_interopNamespaceDefault(eruda);
  44.  
  45. var _GM_openInTab = /* @__PURE__ */ (() => typeof GM_openInTab != "undefined" ? GM_openInTab : void 0)();
  46. var _unsafeWindow = /* @__PURE__ */ (() => typeof unsafeWindow != "undefined" ? unsafeWindow : void 0)();
  47. const _hoisted_2 = {
  48. key: 1,
  49. class: "msg"
  50. };
  51. const _sfc_main = /* @__PURE__ */ vue.defineComponent({
  52. __name: "App",
  53. setup(__props) {
  54. let refVideo = vue.ref(null);
  55. let rate = vue.ref(1);
  56. let lastRate = vue.ref(1);
  57. let pageType = vue.ref("");
  58. let msg = vue.reactive({
  59. show: false,
  60. content: "",
  61. timer: -1
  62. });
  63. function stop(e) {
  64. e.preventDefault();
  65. e.stopPropagation();
  66. e.stopImmediatePropagation();
  67. return true;
  68. }
  69. function openNewTab(href, active = false) {
  70. _GM_openInTab(href, { active });
  71. }
  72. function getBrowserType() {
  73. let userAgent = navigator.userAgent;
  74. if (userAgent.indexOf("Opera") > -1) {
  75. return "Opera";
  76. }
  77. if (userAgent.indexOf("Firefox") > -1) {
  78. return "FF";
  79. }
  80. if (userAgent.indexOf("Chrome") > -1) {
  81. return "Chrome";
  82. }
  83. if (userAgent.indexOf("Safari") > -1) {
  84. return "Safari";
  85. }
  86. if (userAgent.indexOf("compatible") > -1 && userAgent.indexOf("MSIE") > -1 && !isOpera) {
  87. return "IE";
  88. }
  89. }
  90. function initStyle(type) {
  91. let style2 = `
  92. :root {
  93. --color-scrollbar: rgb(147, 173, 227);
  94. }
  95.  
  96. html[darker-dark-theme] {
  97. --color-scrollbar: rgb(92, 93, 94);
  98. }
  99.  
  100. ${type === "FF" ? `/* 火狐美化滚动条 */
  101. * {
  102. scrollbar-color: var(--color-scrollbar);
  103. /* 滑块颜色 滚动条背景颜色 */
  104. scrollbar-width: thin;
  105. /* 滚动条宽度有三种:thin、auto、none */
  106. }` : `
  107. ::-webkit-scrollbar {
  108. width: 1rem;
  109. height: 1rem;
  110. }
  111.  
  112. ::-webkit-scrollbar-track {
  113. background: transparent;
  114. border-radius: .2rem;
  115. }
  116.  
  117. ::-webkit-scrollbar-thumb {
  118. background: var(--color-scrollbar);
  119. border-radius: 1rem;
  120. }`}
  121. `;
  122. let addStyle2 = document.createElement("style");
  123. addStyle2.rel = "stylesheet";
  124. addStyle2.type = "text/css";
  125. addStyle2.innerHTML = style2;
  126. window.document.head.append(addStyle2);
  127. }
  128. function findA(target, e) {
  129. let parentNode = target.parentNode;
  130. let count = 0;
  131. while (parentNode.tagName !== "A" && count < 10) {
  132. count++;
  133. parentNode = parentNode.parentNode;
  134. }
  135. console.log(parentNode);
  136. openNewTab(parentNode.href, true);
  137. return stop(e);
  138. }
  139. function checkPageType() {
  140. if (location.pathname === "/watch") {
  141. pageType.value = "watch";
  142. }
  143. if (location.pathname === "/") {
  144. pageType.value = "home";
  145. }
  146. if (location.pathname.startsWith("/@")) {
  147. pageType.value = "user";
  148. }
  149. }
  150. function checkVideo() {
  151. let v = document.querySelector("video");
  152. if (v) {
  153. v.playbackRate = rate.value;
  154. refVideo.value = v;
  155. window.funs.checkWatchPageDiv();
  156. return true;
  157. }
  158. }
  159. function playbackRateToggle() {
  160. checkVideo();
  161. if (refVideo.value) {
  162. if (refVideo.value.playbackRate !== 1) {
  163. lastRate.value = rate.value;
  164. rate.value = refVideo.value.playbackRate = 1;
  165. showMsg("播放速度: 1");
  166. } else {
  167. rate.value = refVideo.value.playbackRate = lastRate.value === 1 ? 2 : lastRate.value;
  168. showMsg("播放速度: " + rate.value);
  169. }
  170. }
  171. }
  172. function toggle() {
  173. checkVideo();
  174. if (refVideo.value) {
  175. if (refVideo.value.paused) {
  176. refVideo.value.play();
  177. } else {
  178. refVideo.value.pause();
  179. }
  180. }
  181. }
  182. function setPlaybackRate(val) {
  183. checkVideo();
  184. if (refVideo.value) {
  185. rate.value = refVideo.value.playbackRate = Number(val.toFixed(1));
  186. showMsg("播放速度: " + rate.value);
  187. }
  188. }
  189. function showMsg(text) {
  190. if (msg.show) {
  191. msg.show = false;
  192. clearTimeout(msg.timer);
  193. }
  194. msg.show = true;
  195. msg.content = text;
  196. msg.timer = setTimeout(() => {
  197. msg.show = false;
  198. }, 3e3);
  199. }
  200. function checkOptionButtons() {
  201. let dom = document.querySelector(".ytb-next");
  202. if (dom)
  203. return;
  204. dom = document.createElement("div");
  205. dom.classList.add("ytb-next");
  206. dom.innerHTML = `
  207. <div class="btn" onclick="window.cb('playbackRateToggle')">切</div>
  208. <div class="btn" onclick="window.cb('addRate')">&nbsp;+&nbsp;</div>
  209. <div class="btn" onclick="window.cb('removeRate')">&nbsp;-&nbsp;</div>
  210. <div class="btn" onclick="window.cb('playbackRateToggle1')">&nbsp;1&nbsp;</div>
  211. <div class="btn" onclick="window.cb('playbackRateToggle2')">&nbsp;2&nbsp;</div>
  212. <div class="btn" onclick="window.cb('playbackRateToggle25')">&nbsp;2.5&nbsp;</div>
  213. <div class="btn" onclick="window.cb('playbackRateToggle3')">&nbsp;3&nbsp;</div>
  214. `;
  215. document.body.append(dom);
  216. }
  217. function checkIsWatchPage() {
  218. checkPageType();
  219. return pageType.value === "watch";
  220. }
  221. function checkA(e) {
  222. let target = e.target;
  223. let tagName = target.tagName;
  224. let classList = target.classList;
  225. if (tagName === "IMG" && Array.from(classList).some((v) => v.includes("yt-core-image"))) {
  226. console.log("封面");
  227. if (checkIsWatchPage())
  228. return;
  229. return findA(target, e);
  230. }
  231. if (tagName === "SPAN" && Array.from(classList).some((v) => v.includes("yt-core-attributed-string"))) {
  232. console.log("标题");
  233. if (checkIsWatchPage())
  234. return;
  235. return findA(target, e);
  236. }
  237. if (tagName === "BUTTON" && Array.from(classList).some((v) => v.includes("ytp-large-play-button"))) {
  238. console.log("播放按钮");
  239. if (checkIsWatchPage())
  240. return;
  241. }
  242. if (tagName === "DIV" && Array.from(classList).some((v) => v.includes("ytp-cued-thumbnail-overlay-image"))) {
  243. console.log("播放按钮");
  244. if (checkIsWatchPage())
  245. return;
  246. }
  247. }
  248. vue.watch(rate, (value) => {
  249. localStorage.setItem("youtube-rate", value);
  250. window.rate = value;
  251. });
  252. vue.onMounted(() => {
  253. console.log("Youtube Next start");
  254. setTimeout(() => {
  255. let browserType = getBrowserType();
  256. initStyle(browserType);
  257. }, 500);
  258. let youtubeRate = localStorage.getItem("youtube-rate");
  259. if (youtubeRate) {
  260. rate.value = Number(youtubeRate);
  261. }
  262. _unsafeWindow.cb = (type) => {
  263. console.log("type", type);
  264. switch (type) {
  265. case "toggle":
  266. toggle();
  267. break;
  268. case "playbackRateToggle":
  269. playbackRateToggle();
  270. break;
  271. case "playbackRateToggle1":
  272. setPlaybackRate(1);
  273. break;
  274. case "playbackRateToggle2":
  275. setPlaybackRate(2);
  276. break;
  277. case "playbackRateToggle25":
  278. setPlaybackRate(2.5);
  279. break;
  280. case "playbackRateToggle3":
  281. setPlaybackRate(3);
  282. break;
  283. case "addRate":
  284. setPlaybackRate(rate.value + 0.1);
  285. break;
  286. case "removeRate":
  287. setPlaybackRate(rate.value - 0.1);
  288. break;
  289. }
  290. };
  291. if (checkIsWatchPage()) {
  292. setTimeout(() => {
  293. checkOptionButtons();
  294. checkVideo();
  295. if (refVideo.value) {
  296. refVideo.value.muted = false;
  297. refVideo.value.playbackRate = rate.value;
  298. showMsg("播放速度: " + rate.value);
  299. }
  300. }, 1e3);
  301. }
  302. window.addEventListener("click", checkA, true);
  303. window.addEventListener("visibilitychange", stop, true);
  304. });
  305. vue.onUnmounted(() => {
  306. window.removeEventListener("click", checkA, true);
  307. window.removeEventListener("visibilitychange", stop, true);
  308. });
  309. return (_ctx, _cache) => {
  310. return vue.openBlock(), vue.createElementBlock(vue.Fragment, null, [
  311. vue.createCommentVNode("", true),
  312. vue.unref(msg).show ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_2, vue.toDisplayString(vue.unref(msg).content), 1)) : vue.createCommentVNode("", true)
  313. ], 64);
  314. };
  315. }
  316. });
  317. try {
  318. if (window.trustedTypes && window.trustedTypes.createPolicy) {
  319. window.trustedTypes.createPolicy("default", {
  320. createHTML: (string) => string,
  321. createScriptURL: (string) => string,
  322. createScript: (string) => string
  323. });
  324. }
  325. } catch (e) {
  326. console.error("trustedTypes");
  327. }
  328. try {
  329. setTimeout(() => {
  330. var _a;
  331. (_a = window.eruda) == null ? void 0 : _a.init();
  332. eruda__namespace == null ? void 0 : eruda__namespace.init();
  333. }, 0);
  334. } catch (e) {
  335. }
  336. window.videoEl = null;
  337. window.rate = 1;
  338. window.funs = {
  339. checkWatchPageDiv() {
  340. let header = document.querySelector("#header-bar");
  341. let stickyPlayer = document.querySelector("#app.sticky-player");
  342. if (header)
  343. header.style["display"] = "none";
  344. if (stickyPlayer)
  345. stickyPlayer.style["padding-top"] = "0";
  346. }
  347. };
  348. async function sleep(time) {
  349. return new Promise((resolve) => {
  350. setTimeout(resolve, time);
  351. });
  352. }
  353. function proxyHTMLMediaElementEvent() {
  354. if (HTMLMediaElement.prototype._rawAddEventListener_) {
  355. return false;
  356. }
  357. HTMLMediaElement.prototype._rawAddEventListener_ = HTMLMediaElement.prototype.addEventListener;
  358. HTMLMediaElement.prototype._rawRemoveEventListener_ = HTMLMediaElement.prototype.removeEventListener;
  359. HTMLMediaElement.prototype.addEventListener = new Proxy(HTMLMediaElement.prototype.addEventListener, {
  360. apply(target, ctx, args) {
  361. const eventName = args[0];
  362. const listener = args[1];
  363. if (listener instanceof Function && eventName === "ratechange") {
  364. args[1] = new Proxy(listener, {
  365. apply(target2, ctx2, args2) {
  366. if (ctx2) {
  367. if (ctx2.playbackRate && eventName === "ratechange") {
  368. if (ctx2._hasBlockRatechangeEvent_) {
  369. return true;
  370. }
  371. const oldRate = ctx2.playbackRate;
  372. const startTime = Date.now();
  373. const result = target2.apply(ctx2, args2);
  374. const blockRatechangeBehave1 = oldRate !== ctx2.playbackRate || Date.now() - startTime > 1e3;
  375. const blockRatechangeBehave2 = ctx2._setPlaybackRate_ && ctx2._setPlaybackRate_.value !== ctx2.playbackRate;
  376. if (blockRatechangeBehave1 || blockRatechangeBehave2) {
  377. debug.info(`[execVideoEvent][${eventName}]检测到可能存在阻止调速的行为,已禁止${eventName}事件的执行`, listener);
  378. ctx2._hasBlockRatechangeEvent_ = true;
  379. return true;
  380. } else {
  381. return result;
  382. }
  383. }
  384. }
  385. try {
  386. return target2.apply(ctx2, args2);
  387. } catch (e) {
  388. debug.error(`[proxyPlayerEvent][${eventName}]`, listener, e);
  389. }
  390. }
  391. });
  392. }
  393. if (listener instanceof Function && eventName === "play") {
  394. args[1] = new Proxy(listener, {
  395. apply(target2, ctx2, args2) {
  396. console.log("play", window.rate);
  397. ctx2.playbackRate = window.rate;
  398. window.funs.checkWatchPageDiv();
  399. try {
  400. return target2.apply(ctx2, args2);
  401. } catch (e) {
  402. debug.error(`[proxyPlayerEvent][${eventName}]`, listener, e);
  403. }
  404. }
  405. });
  406. }
  407. return target.apply(ctx, args);
  408. }
  409. });
  410. }
  411. proxyHTMLMediaElementEvent();
  412. async function init() {
  413. let $section = document.createElement("section");
  414. $section.id = "vue-app";
  415. let count = 0;
  416. if (document.body) {
  417. document.body.append($section);
  418. } else {
  419. while (!document.body && count < 50) {
  420. await sleep(100);
  421. count++;
  422. }
  423. document.body.append($section);
  424. }
  425. let vueApp = vue.createApp(_sfc_main);
  426. vueApp.config.unwrapInjectedRef = true;
  427. vueApp.mount($section);
  428. }
  429. init();
  430.  
  431. })(Vue, Vue);

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址