// ==UserScript==
// @name Youtube Mobile Enhance 油管移动端增强
// @namespace http://tampermonkey.net/
// @version 2.9.2
// @author zyronon
// @description 针对油管移动端,点击视频新标签页打开,记忆播放速度,突破播放速度限制
// @license GPL License
// @icon https://v2next.netlify.app/favicon.ico
// @homepage https://github.com/zyronon/web-scripts
// @homepageURL https://github.com/zyronon/web-scripts
// @supportURL https://update.greasyfork.org/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
// @match https://m.youtube.com/*
// @require https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.global.prod.js
// @require https://cdn.jsdelivr.net/npm/[email protected]/eruda.js
// @grant GM_addStyle
// @grant GM_openInTab
// @grant unsafeWindow
// @run-at document-start
// ==/UserScript==
(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}} ");
(function (vue, eruda) {
'use strict';
function _interopNamespaceDefault(e) {
const n = Object.create(null, { [Symbol.toStringTag]: { value: 'Module' } });
if (e) {
for (const k in e) {
if (k !== 'default') {
const d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: () => e[k]
});
}
}
}
n.default = e;
return Object.freeze(n);
}
const eruda__namespace = /*#__PURE__*/_interopNamespaceDefault(eruda);
var _GM_openInTab = /* @__PURE__ */ (() => typeof GM_openInTab != "undefined" ? GM_openInTab : void 0)();
var _unsafeWindow = /* @__PURE__ */ (() => typeof unsafeWindow != "undefined" ? unsafeWindow : void 0)();
const _hoisted_2 = {
key: 1,
class: "msg"
};
const _sfc_main = /* @__PURE__ */ vue.defineComponent({
__name: "App",
setup(__props) {
let refVideo = vue.ref(null);
let rate = vue.ref(1);
let lastRate = vue.ref(1);
let pageType = vue.ref("");
let msg = vue.reactive({
show: false,
content: "",
timer: -1
});
function stop(e) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
return true;
}
function openNewTab(href, active = false) {
_GM_openInTab(href, { active });
}
function getBrowserType() {
let userAgent = navigator.userAgent;
if (userAgent.indexOf("Opera") > -1) {
return "Opera";
}
if (userAgent.indexOf("Firefox") > -1) {
return "FF";
}
if (userAgent.indexOf("Chrome") > -1) {
return "Chrome";
}
if (userAgent.indexOf("Safari") > -1) {
return "Safari";
}
if (userAgent.indexOf("compatible") > -1 && userAgent.indexOf("MSIE") > -1 && !isOpera) {
return "IE";
}
}
function initStyle(type) {
let style2 = `
:root {
--color-scrollbar: rgb(147, 173, 227);
}
html[darker-dark-theme] {
--color-scrollbar: rgb(92, 93, 94);
}
${type === "FF" ? `/* 火狐美化滚动条 */
* {
scrollbar-color: var(--color-scrollbar);
/* 滑块颜色 滚动条背景颜色 */
scrollbar-width: thin;
/* 滚动条宽度有三种:thin、auto、none */
}` : `
::-webkit-scrollbar {
width: 1rem;
height: 1rem;
}
::-webkit-scrollbar-track {
background: transparent;
border-radius: .2rem;
}
::-webkit-scrollbar-thumb {
background: var(--color-scrollbar);
border-radius: 1rem;
}`}
`;
let addStyle2 = document.createElement("style");
addStyle2.rel = "stylesheet";
addStyle2.type = "text/css";
addStyle2.innerHTML = style2;
window.document.head.append(addStyle2);
}
function findA(target, e) {
let parentNode = target.parentNode;
let count = 0;
while (parentNode.tagName !== "A" && count < 10) {
count++;
parentNode = parentNode.parentNode;
}
console.log(parentNode);
openNewTab(parentNode.href, true);
return stop(e);
}
function checkPageType() {
if (location.pathname === "/watch") {
pageType.value = "watch";
}
if (location.pathname === "/") {
pageType.value = "home";
}
if (location.pathname.startsWith("/@")) {
pageType.value = "user";
}
}
function checkVideo() {
let v = document.querySelector("video");
if (v) {
v.playbackRate = rate.value;
refVideo.value = v;
window.funs.checkWatchPageDiv();
return true;
}
}
function playbackRateToggle() {
checkVideo();
if (refVideo.value) {
if (refVideo.value.playbackRate !== 1) {
lastRate.value = rate.value;
rate.value = refVideo.value.playbackRate = 1;
showMsg("播放速度: 1");
} else {
rate.value = refVideo.value.playbackRate = lastRate.value === 1 ? 2 : lastRate.value;
showMsg("播放速度: " + rate.value);
}
}
}
function toggle() {
checkVideo();
if (refVideo.value) {
if (refVideo.value.paused) {
refVideo.value.play();
} else {
refVideo.value.pause();
}
}
}
function setPlaybackRate(val) {
checkVideo();
if (refVideo.value) {
rate.value = refVideo.value.playbackRate = Number(val.toFixed(1));
showMsg("播放速度: " + rate.value);
}
}
function showMsg(text) {
if (msg.show) {
msg.show = false;
clearTimeout(msg.timer);
}
msg.show = true;
msg.content = text;
msg.timer = setTimeout(() => {
msg.show = false;
}, 3e3);
}
function checkOptionButtons() {
let dom = document.querySelector(".ytb-next");
if (dom)
return;
dom = document.createElement("div");
dom.classList.add("ytb-next");
dom.innerHTML = `
<div class="btn" onclick="window.cb('playbackRateToggle')">切</div>
<div class="btn" onclick="window.cb('addRate')"> + </div>
<div class="btn" onclick="window.cb('removeRate')"> - </div>
<div class="btn" onclick="window.cb('playbackRateToggle1')"> 1 </div>
<div class="btn" onclick="window.cb('playbackRateToggle2')"> 2 </div>
<div class="btn" onclick="window.cb('playbackRateToggle25')"> 2.5 </div>
<div class="btn" onclick="window.cb('playbackRateToggle3')"> 3 </div>
`;
document.body.append(dom);
}
function checkIsWatchPage() {
checkPageType();
return pageType.value === "watch";
}
function checkA(e) {
let target = e.target;
let tagName = target.tagName;
let classList = target.classList;
if (tagName === "IMG" && Array.from(classList).some((v) => v.includes("yt-core-image"))) {
console.log("封面");
if (checkIsWatchPage())
return;
return findA(target, e);
}
if (tagName === "SPAN" && Array.from(classList).some((v) => v.includes("yt-core-attributed-string"))) {
console.log("标题");
if (checkIsWatchPage())
return;
return findA(target, e);
}
if (tagName === "BUTTON" && Array.from(classList).some((v) => v.includes("ytp-large-play-button"))) {
console.log("播放按钮");
if (checkIsWatchPage())
return;
}
if (tagName === "DIV" && Array.from(classList).some((v) => v.includes("ytp-cued-thumbnail-overlay-image"))) {
console.log("播放按钮");
if (checkIsWatchPage())
return;
}
}
vue.watch(rate, (value) => {
localStorage.setItem("youtube-rate", value);
window.rate = value;
});
vue.onMounted(() => {
console.log("Youtube Next start");
setTimeout(() => {
let browserType = getBrowserType();
initStyle(browserType);
}, 500);
let youtubeRate = localStorage.getItem("youtube-rate");
if (youtubeRate) {
rate.value = Number(youtubeRate);
}
_unsafeWindow.cb = (type) => {
console.log("type", type);
switch (type) {
case "toggle":
toggle();
break;
case "playbackRateToggle":
playbackRateToggle();
break;
case "playbackRateToggle1":
setPlaybackRate(1);
break;
case "playbackRateToggle2":
setPlaybackRate(2);
break;
case "playbackRateToggle25":
setPlaybackRate(2.5);
break;
case "playbackRateToggle3":
setPlaybackRate(3);
break;
case "addRate":
setPlaybackRate(rate.value + 0.1);
break;
case "removeRate":
setPlaybackRate(rate.value - 0.1);
break;
}
};
if (checkIsWatchPage()) {
setTimeout(() => {
checkOptionButtons();
checkVideo();
if (refVideo.value) {
refVideo.value.muted = false;
refVideo.value.playbackRate = rate.value;
showMsg("播放速度: " + rate.value);
}
}, 1e3);
}
window.addEventListener("click", checkA, true);
window.addEventListener("visibilitychange", stop, true);
});
vue.onUnmounted(() => {
window.removeEventListener("click", checkA, true);
window.removeEventListener("visibilitychange", stop, true);
});
return (_ctx, _cache) => {
return vue.openBlock(), vue.createElementBlock(vue.Fragment, null, [
vue.createCommentVNode("", true),
vue.unref(msg).show ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_2, vue.toDisplayString(vue.unref(msg).content), 1)) : vue.createCommentVNode("", true)
], 64);
};
}
});
try {
if (window.trustedTypes && window.trustedTypes.createPolicy) {
window.trustedTypes.createPolicy("default", {
createHTML: (string) => string,
createScriptURL: (string) => string,
createScript: (string) => string
});
}
} catch (e) {
console.error("trustedTypes");
}
try {
setTimeout(() => {
var _a;
(_a = window.eruda) == null ? void 0 : _a.init();
eruda__namespace == null ? void 0 : eruda__namespace.init();
}, 0);
} catch (e) {
}
window.videoEl = null;
window.rate = 1;
window.funs = {
checkWatchPageDiv() {
let header = document.querySelector("#header-bar");
let stickyPlayer = document.querySelector("#app.sticky-player");
if (header)
header.style["display"] = "none";
if (stickyPlayer)
stickyPlayer.style["padding-top"] = "0";
}
};
async function sleep(time) {
return new Promise((resolve) => {
setTimeout(resolve, time);
});
}
function proxyHTMLMediaElementEvent() {
if (HTMLMediaElement.prototype._rawAddEventListener_) {
return false;
}
HTMLMediaElement.prototype._rawAddEventListener_ = HTMLMediaElement.prototype.addEventListener;
HTMLMediaElement.prototype._rawRemoveEventListener_ = HTMLMediaElement.prototype.removeEventListener;
HTMLMediaElement.prototype.addEventListener = new Proxy(HTMLMediaElement.prototype.addEventListener, {
apply(target, ctx, args) {
const eventName = args[0];
const listener = args[1];
if (listener instanceof Function && eventName === "ratechange") {
args[1] = new Proxy(listener, {
apply(target2, ctx2, args2) {
if (ctx2) {
if (ctx2.playbackRate && eventName === "ratechange") {
if (ctx2._hasBlockRatechangeEvent_) {
return true;
}
const oldRate = ctx2.playbackRate;
const startTime = Date.now();
const result = target2.apply(ctx2, args2);
const blockRatechangeBehave1 = oldRate !== ctx2.playbackRate || Date.now() - startTime > 1e3;
const blockRatechangeBehave2 = ctx2._setPlaybackRate_ && ctx2._setPlaybackRate_.value !== ctx2.playbackRate;
if (blockRatechangeBehave1 || blockRatechangeBehave2) {
debug.info(`[execVideoEvent][${eventName}]检测到可能存在阻止调速的行为,已禁止${eventName}事件的执行`, listener);
ctx2._hasBlockRatechangeEvent_ = true;
return true;
} else {
return result;
}
}
}
try {
return target2.apply(ctx2, args2);
} catch (e) {
debug.error(`[proxyPlayerEvent][${eventName}]`, listener, e);
}
}
});
}
if (listener instanceof Function && eventName === "play") {
args[1] = new Proxy(listener, {
apply(target2, ctx2, args2) {
console.log("play", window.rate);
ctx2.playbackRate = window.rate;
window.funs.checkWatchPageDiv();
try {
return target2.apply(ctx2, args2);
} catch (e) {
debug.error(`[proxyPlayerEvent][${eventName}]`, listener, e);
}
}
});
}
return target.apply(ctx, args);
}
});
}
proxyHTMLMediaElementEvent();
async function init() {
let $section = document.createElement("section");
$section.id = "vue-app";
let count = 0;
if (document.body) {
document.body.append($section);
} else {
while (!document.body && count < 50) {
await sleep(100);
count++;
}
document.body.append($section);
}
let vueApp = vue.createApp(_sfc_main);
vueApp.config.unwrapInjectedRef = true;
vueApp.mount($section);
}
init();
})(Vue, Vue);