// ==UserScript== // @name 视频小窗 // @namespace http://tampermonkey.net/ // @version 0.9.4 // @author Feny // @description 「视频自动网页全屏|倍速播放」脚本的功能扩展,提供全站通用页内悬浮视频小窗支持,可自由拖拽摆放位置。 // @license GPL-3.0-only // @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAhBJREFUWEfdl79PFEEUx7/v+BtMCMZIRXE35HYuFiY0xz9Ad3aKBewQE6koTcTa2LMLJIodJJqQkBgbaSB0c4Y5OwtLKPwDIOwzc2RlOe529ti948e2+953PvN9M2/fEm74oRteH7cLQEi1RsBTBioFObMHpjemubLbS++/A0LOPwdKG2B+B5R6JvQDxsQBARN2Qy0d/OqWmwBQ2wA/Njqs9rNIWqyUrx+c4uQY4CWjww8ugB82wOhguigAqyOkYuuqaYbLdw9AeP4yiMpGB8+S9EL668T4dtgMt1xu5XJg0vMbTLQJYCsJ4RK9DJuzBN0ghgpgd9MJMXSATggAjbSTXWgJhLdQvxCM6gyME9HsUACEVPYANrqe9JS7PUAHLqTT+nsSoOKpVyDsZmnFA+mErj6R/BZcG6DsLbwsIRp3LQbi33wS7bRa63/j2NwA7SvZ73N6NmrM2pFNywUgauo9GEvgaMo0V/ddHBWpygR8B/DF6GAxP4BUfZdNdOTkc+B+AdTUVzA/Mjp84qpl/L7Tzix5vUtw/u1/m2UmjJtQoQDt8SmGcGzFaDtrtset4g5hvGa1Oj9xRiNjaQwJBzYJNHWoVx5msf8c2v8J0B+jg5lL1zCrQDJusub7zBQw4SNF9MmtEdXbZUb0wujVz7kB4jnBimb5mSGgxcCB0cHclVbsph9MxO36NxzMHtNV/wFo/XswLTIqPQAAAABJRU5ErkJggg== // @match *://*/* // @require https://unpkg.com/draggable@4.2.0/dist/draggable.min.js // @grant GM_addStyle // @grant GM_addValueChangeListener // @grant GM_getValue // @grant GM_registerMenuCommand // @grant GM_setValue // @grant GM_unregisterMenuCommand // @grant unsafeWindow // @run-at document-body // @noframes // ==/UserScript== (n=>{if(typeof GM_addStyle=="function"){GM_addStyle(n);return}const o=document.createElement("style");o.textContent=n,document.head.append(o)})(' @charset "UTF-8";.vc-nano-wrap{right:80px;bottom:80px;display:none;position:fixed;min-width:500px;min-height:330px;pointer-events:none;z-index:2147483646!important}.vc-nano-wrap.active{display:block}.vc-nano-wrap:hover .vc-nano-header{background-color:#202124}.vc-nano-wrap:hover .vc-nano-close{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAHRJREFUOE+tU0EOgCAMa19mfJn4MuPLapZgMmUYw+DIutJ2jEgeJvsxl0DSAeAkWSJlkux+Ibne9YeCCtgA7G+SXq2x4ICF5G4vfRGHGfiGKjVUZbVuiI7EcI2lMAMfXIogZSEK7HeI6TGmP9LIXszdhREFF1FhUBFxTxg9AAAAAElFTkSuQmCC)}.vc-nano-wrap:hover .vc-nano-back{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAANFJREFUOE/Vkz0OgUEQhp/3AjqnUDoACoVGR6emkSh1PqVSqyJRcQii1qodZWRkN1ksvkRlqk3mnWd+V/xo8ngzOwP1hHUB+kAN2GVyrIGNpGMEWBDNgJO/3RngzQygETTzFOAZPdtU0uJTZ2bm0IPcQhavoAVUA2QiafkO8hbgZZtZL0CGklY5SA7Qk7SP4gQykLR9huQAhaQiFUZIbPPJl59BnPy30/g4g2/ByWpft/DfFVz9toH7+ZawNlCRNI6X2AVGQKdEsEseP1PJoKzsBhX1hhF94V4PAAAAAElFTkSuQmCC)}.vc-nano-header{cursor:move;height:30px;position:relative;pointer-events:all!important;background-color:transparent}.vc-nano-back,.vc-nano-close{top:3px;width:24px;height:24px;cursor:pointer;position:absolute;border-radius:50%;display:inline-block;background-position:center;background-repeat:no-repeat;background-size:13px}.vc-nano-back:hover,.vc-nano-close:hover{background-color:#3c3d3f}.vc-nano-close{right:5px}.vc-nano-back{right:29px}.vc-nano-content{position:absolute!important;background:#000!important;pointer-events:all!important;box-shadow:0 2px 8px #00000080}.vc-nano-player{width:inherit!important;height:inherit!important}.vc-nano-player *:has(>video){width:100%!important;height:100%!important}.vc-nano-player video{width:100%!important;height:100%!important;position:unset!important}.vc-nano-player video:not(.__tsr){transform:none!important} '); (function (Draggable) { 'use strict'; class BasicStorage { constructor(name, defVal, parser = (v) => v) { Object.assign(this, { name, defVal, parser }); } #getKey(suffix = "") { return this.name + suffix; } set(value, suffix) { GM_setValue(this.#getKey(suffix), value); } get(suffix) { const val = GM_getValue(this.#getKey(suffix)); return this.parser(val ?? this.defVal); } } const Store = { ENABLE_NANO: new BasicStorage("ENABLE_NANO_PLAYER_", false), INTERSECT_ELEMENT: new BasicStorage("INTERSECT_ELEMENT_", ""), NANO_SIZE: new BasicStorage("ENABLE_NANO", "500,300", (v) => v.split(",")), IGNORE_URLS: new BasicStorage("IGNORE_URLS_", "", (v) => v.split(/[,;]/).map((s) => s.trim())) }; const Menu = { initMenuCmds() { if (FyTools.isExecuted("hasMenu", window)) return; GM_addValueChangeListener(Store.ENABLE_NANO.name + this.host, () => this.setupMenuCmds()); this.setupMenuCmds(); }, setupMenuCmds() { const enTle = `此站${Store.ENABLE_NANO.get(this.host) ? "禁" : "启"}用悬浮小窗`; const configs = [ { title: "设置小窗的宽高", cache: Store.NANO_SIZE, fn: this.inputNanoSize }, { title: "小窗视口监测元素", cache: Store.INTERSECT_ELEMENT, useHost: true, fn: this.setIntersect }, { title: enTle, cache: Store.ENABLE_NANO, useHost: true, fn: this.setNanoEnabled }, { title: "此站网址黑名单", cache: Store.IGNORE_URLS, useHost: true } ]; configs.forEach(({ title, isHide, useHost, cache, fn }) => { const id = `${cache.name}_MENU_ID`; GM_unregisterMenuCommand(this[id]); if (isHide) return; const host = useHost ? this.host : ""; this[id] = GM_registerMenuCommand(title, () => { if (fn) return fn.call(this, { host, cache, title }); const input = prompt(title, cache.get(host)); if (input !== null) cache.set(input, host); }); }); }, setIntersect({ host, cache, title }) { const input = prompt(title, cache.get(host)); if (input !== null) cache.set(input, host), this.createNanoObserver(); }, setNanoEnabled({ host, cache, title }) { const isEnable = !cache.get(host); isEnable ? this.createNanoObserver() : this.activateNano(false); cache.set(isEnable, host); }, inputNanoSize({ host, cache, title }) { const input = prompt(title, cache.get(host)); if (input !== null) cache.set(input, host), this.setNanoStyleSize(); } }; const Utils = { onBefore(target, funcName, exec) { this.around(target, funcName, (original, args) => { Promise.resolve().then(() => exec.apply(this, args)); return original(...args); }); }, around(target, funcName, wrapper) { const original = target[funcName]; target[funcName] = function(...args) { return wrapper.call(this, original.bind(this), args); }; } }; class NanoFloatWindow { constructor(options = {}) { Object.assign(this, { width: 500, height: 300 }, options); this.#init(); } #init() { if (!this.target) return; this.#cacheOrigin(); this.#createElement(); this.#bindDraggable(); this.activate(false); this.setSize(); return this; } #cacheOrigin() { this.originParent = this.target.parentElement; this.originNext = this.target.nextSibling; } #newEle = (tag, attrs) => Object.assign(document.createElement(tag), attrs); #createElement() { if (this.wrap) return; this.header = this.#buildHeader(); this.content = this.#newEle("div", { className: "vc-nano-content" }); this.wrap = this.#newEle("div", { className: "vc-nano-wrap" }); this.wrap.append(this.header, this.content); document.body.prepend(this.wrap); } #buildHeader() { const close = this.#newEle("span", { title: "关闭", className: "vc-nano-close", onclick: () => this.#close() }); const back = this.#newEle("span", { title: "返回", className: "vc-nano-back", onclick: () => this.#goBack() }); const header = this.#newEle("div", { className: "vc-nano-header" }); header.append(close, back); return header; } #close() { const y = window.scrollY; this.activate(false); window.scrollTo(0, y); } #goBack() { this.activate(false); if (!this.target?.isConnected) return; this.target.scrollIntoView({ block: "center" }); } #bindDraggable() { if (!this.header || !this.content) return; new Draggable(this.header, { setPosition: false, onDrag: (_, x, y) => { this.content.style.left = `${x}px`; this.content.style.top = `${y + this.header.offsetHeight}px`; } }); } setSize(w = this.width, h = this.height) { if (!this.content) return; this.header.style.width = this.content.style.width = `${w > 0 ? w : this.width}px`; this.content.style.height = `${h > 0 ? h : this.height}px`; } activate(show) { const { target } = this; if (!this.wrap || !target) return; try { const parent = show ? this.content : this.originParent; if (!target?.isConnected || !parent?.isConnected) return; parent?.moveBefore(target, show ? null : this.originNext); target.classList.toggle("vc-nano-player", show); this.wrap.classList.toggle("active", show); } catch (err) { console.warn("页内小窗切换异常:", err); this.#resetContent(); } } isActive = () => this.content.hasChildNodes(); setTarget(target) { if (!(target instanceof HTMLElement)) return; if (this.target === target) return; this.#resetContent(); this.target = target; this.#cacheOrigin(); } #resetContent() { try { this.content?.replaceChildren(); this.wrap?.classList.remove("active"); this.target?.classList.remove("vc-nano-player"); this.target = null; } catch (e) { } } } const Main = { FS: null, init() { if (!Element.prototype.moveBefore) return console.warn("浏览器不支持!!"); if (!unsafeWindow.GM_E9X_FS) return console.warn("未安装依赖,无法正常运行!!"); this.FS = unsafeWindow.GM_E9X_FS; this.setupNavigateListener(); this.setupTopWinListener(); this.lockedWebFullscreen(); this.host = location.host; }, setupNavigateListener() { navigation.addEventListener("navigate", () => { if (!this.nano) return; this.activateNano(false, true); FyTools.scrollTop(0); }); }, lockedWebFullscreen() { Utils.around(this.FS, "processEvent", (original, args) => { if (this.nano?.isActive() && ["P", "ENTER"].includes(args[0]?.key)) return; return original(...args); }); }, setupTopWinListener() { Utils.onBefore(this.FS, "syncMetaToParentWin", () => this.setupNanoFeatures()); unsafeWindow.addEventListener("load", () => this.FS.topWin && this.setupNanoFeatures()); }, setupNanoFeatures() { try { this.initMenuCmds(); this.createNanoObserver(); } catch (err) { console.warn(err); } }, createNanoObserver() { if (this.nano) this.activateNano(false); const target = this.FS.getVideoHostContainer(); if (!target?.isConnected) return console.warn("元素不存在或已销毁"); this.nano ??= new NanoFloatWindow({ target }); if (this.observer) this.nano.setTarget(target); const obsNode = this.getInter(target, FyTools.getParent(target)); this.setupNanoObserver(obsNode); }, setupNanoObserver(obsNode) { if (!obsNode) return; this.observer?.disconnect(); this.observer = new IntersectionObserver( ([entry]) => { if (!obsNode?.isConnected) return this.createNanoObserver(); if (this.isBlackUrl() || !Store.ENABLE_NANO.get(this.host)) return; this.activateNano(!entry.isIntersecting); this.setNanoStyleSize(); }, { root: null, threshold: 0 } ); this.observer.observe(obsNode); }, getInter(ctx, defVal) { const selector = Store.INTERSECT_ELEMENT.get(this.host); return selector ? ctx?.closest(selector) ?? FyTools.query(selector) : defVal; }, setNanoStyleSize() { const [w, h] = Store.NANO_SIZE.get(); this.nano?.setSize(w, h); }, activateNano(active) { this.nano?.activate(active); (this.isNotFirst || this.nano?.isActive()) && this.FS.sendToVideoIFrame({ key: "P" }); this.isNotFirst = true; }, isBlackUrl() { const { href, pathname } = location; const uris = Store.IGNORE_URLS.get(this.host); const isBlack = uris.some((prefix) => prefix && href.startsWith(prefix)); return isBlack || Object.is(pathname, "/"); } }; const App = { ...Main, ...Menu }; App.init(); })(Draggable);