// ==UserScript== // @name HTML5视频播放器增强脚本 // @name:en HTML5 video player enhanced script // @name:zh HTML5视频播放器增强脚本 // @name:zh-TW HTML5視頻播放器增強腳本 // @name:ja HTML5ビデオプレーヤーの拡張スクリプト // @name:ko HTML5 비디오 플레이어 고급 스크립트 // @name:ru HTML5 видео плеер улучшенный скрипт // @name:de HTML5 Video Player erweitertes Skript // @namespace https://github.com/xxxily/h5player // @homepage https://github.com/xxxily/h5player // @version 4.2.2 // @description 视频增强脚本,支持所有H5视频网站,例如:B站、抖音、腾讯视频、优酷、爱奇艺、西瓜视频、油管(YouTube)、微博视频、知乎视频、搜狐视频、网易公开课、百度网盘、阿里云盘、ted、instagram、twitter等。全程快捷键控制,支持:倍速播放/加速播放、视频画面截图、画中画、网页全屏、调节亮度、饱和度、对比度、自定义配置功能增强等功能,为你提供愉悦的在线视频播放体验。还有视频广告快进、在线教程/教育视频倍速快学、视频文件下载等能力 // @description:en Video enhancement script, supports all H5 video websites, such as: Bilibili, Douyin, Tencent Video, Youku, iQiyi, Xigua Video, YouTube, Weibo Video, Zhihu Video, Sohu Video, NetEase Open Course, Baidu network disk, Alibaba cloud disk, ted, instagram, twitter, etc. Full shortcut key control, support: double-speed playback/accelerated playback, video screenshots, picture-in-picture, full-screen web pages, adjusting brightness, saturation, contrast // @description:zh 视频增强脚本,支持所有H5视频网站,例如:B站、抖音、腾讯视频、优酷、爱奇艺、西瓜视频、油管(YouTube)、微博视频、知乎视频、搜狐视频、网易公开课、百度网盘、阿里云盘、ted、instagram、twitter等。全程快捷键控制,支持:倍速播放/加速播放、视频画面截图、画中画、网页全屏、调节亮度、饱和度、对比度、自定义配置功能增强等功能,为你提供愉悦的在线视频播放体验。还有视频广告快进、在线教程/教育视频倍速快学、视频文件下载等能力 // @description:zh-TW 視頻增強腳本,支持所有H5視頻網站,例如:B站、抖音、騰訊視頻、優酷、愛奇藝、西瓜視頻、油管(YouTube)、微博視頻、知乎視頻、搜狐視頻、網易公開課、百度網盤、阿里雲盤、ted、instagram、twitter等。全程快捷鍵控制,支持:倍速播放/加速播放、視頻畫面截圖、畫中畫、網頁全屏、調節亮度、飽和度、對比度、自定義配置功能增強等功能,為你提供愉悅的在線視頻播放體驗。還有視頻廣告快進、在線教程/教育視頻倍速快學、視頻文件下載等能力 // @description:ja ビデオ拡張スクリプトは、Bilibili、Douyin、Tencent Video、Youku、iQiyi、Xigua Video、YouTube、Weibo Video、Zhihu Video、Sohu Video、NetEase Open Course、Baidu ネットワーク ディスク、Alibaba クラウド ディスクなど、すべての H5 ビデオ Web サイトをサポートします。テッド、インスタグラム、ツイッターなど 完全なショートカット キー コントロール、サポート: 倍速再生/加速再生、ビデオ スクリーンショット、ピクチャー イン ピクチャー、フルスクリーン Web ページ、明るさ、彩度、コントラストの調整、カスタム構成の強化、その他の機能により、快適なオンラインを提供します。ビデオ再生体験。 ビデオ広告、オンライン チュートリアル/教育ビデオなどを早送りする機能もあります。 // @description:ko 비디오 향상 스크립트는 Bilibili, Douyin, Tencent Video, Youku, iQiyi, Xigua Video, YouTube, Weibo Video, Zhihu Video, Sohu Video, NetEase Open Course, Baidu 네트워크 디스크, Alibaba 클라우드 디스크와 같은 모든 H5 비디오 웹사이트를 지원합니다. 테드, 인스타그램, 트위터 등 전체 바로 1가기 키 제어, 지원: 배속 재생/가속 재생, 비디오 스크린샷, PIP(Picture-in-Picture), 전체 화면 웹 페이지, 밝기, 채도, 대비, 사용자 정의 구성 향상 및 기타 기능 조정, 쾌적한 온라인 환경 제공 비디오 재생 경험. 비디오 광고, 온라인 자습서/교육 비디오 등을 빨리 감기하는 기능도 있습니다. // @description:ru Сценарий улучшения видео поддерживает все видео-сайты H5, такие как: Bilibili, Douyin, Tencent Video, Youku, iQiyi, Xigua Video, YouTube, Weibo Video, Zhihu Video, Sohu Video, NetEase Open Course, сетевой диск Baidu, облачный диск Alibaba, Тед, инстаграм, твиттер и т.д. Полное управление клавишами быстрого доступа, поддержка: воспроизведение с удвоенной скоростью/ускоренное воспроизведение, скриншоты видео, картинка в картинке, полноэкранные веб-страницы // @description:de Videoverbesserungsskript, unterstützt alle H5-Videowebsites, wie z. ted, instagram, twitter usw. Vollständige Tastenkombinationssteuerung, Unterstützung: Wiedergabe mit doppelter Geschwindigkeit/beschleunigte Wiedergabe, Video-Screenshots, Bild-in-Bild, Vollbild-Webseiten, Anpassung von Helligkeit, Sättigung, Kontrast, benutzerdefinierte Konfigurationsverbesserungen und andere Funktionen // @author ankvps // @icon data:image/png;base64,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 // @match *://*/* // @exclude *://yiyan.baidu.com/* // @exclude *://*.bing.com/search* // @grant unsafeWindow // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_listValues // @grant GM_addValueChangeListener // @grant GM_removeValueChangeListener // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @grant GM_getTab // @grant GM_saveTab // @grant GM_getTabs // @grant GM_openInTab // @grant GM_setClipboard // @run-at document-start // @antifeature ads // @license GPL // @downloadURL none // ==/UserScript== (function (w) { if (w) { w.name = 'h5player'; } })(); /* 保存重要的原始函数,防止被外部脚本污染 */ const originalMethods = { Object: { defineProperty: Object.defineProperty, defineProperties: Object.defineProperties }, setInterval: window.setInterval, setTimeout: window.setTimeout, HTMLElement: window.HTMLElement, customElements: window.customElements, customElementsMethods: { define: window.customElements.define, get: window.customElements.get } }; /** * 元素监听器 * @param selector -必选 * @param fn -必选,元素存在时的回调 * @param shadowRoot -可选 指定监听某个shadowRoot下面的DOM元素 * 参考:https://javascript.ruanyifeng.com/dom/mutationobserver.html */ function ready (selector, fn, shadowRoot) { const win = window; const docRoot = shadowRoot || win.document.documentElement; if (!docRoot) return false const MutationObserver = win.MutationObserver || win.WebKitMutationObserver; const listeners = docRoot._MutationListeners || []; function $ready (selector, fn) { // 储存选择器和回调函数 listeners.push({ selector: selector, fn: fn }); /* 增加监听对象 */ if (!docRoot._MutationListeners || !docRoot._MutationObserver) { docRoot._MutationListeners = listeners; docRoot._MutationObserver = new MutationObserver(() => { for (let i = 0; i < docRoot._MutationListeners.length; i++) { const item = docRoot._MutationListeners[i]; check(item.selector, item.fn); } }); docRoot._MutationObserver.observe(docRoot, { childList: true, subtree: true }); } // 检查节点是否已经在DOM中 check(selector, fn); } function check (selector, fn) { const elements = docRoot.querySelectorAll(selector); for (let i = 0; i < elements.length; i++) { const element = elements[i]; element._MutationReadyList_ = element._MutationReadyList_ || []; if (!element._MutationReadyList_.includes(fn)) { element._MutationReadyList_.push(fn); fn.call(element, element); } } } const selectorArr = Array.isArray(selector) ? selector : [selector]; selectorArr.forEach(selector => $ready(selector, fn)); } /** * 某些网页用了attachShadow closed mode,需要open才能获取video标签,例如百度云盘 * 解决参考: * https://developers.google.com/web/fundamentals/web-components/shadowdom?hl=zh-cn#closed * https://stackoverflow.com/questions/54954383/override-element-prototype-attachshadow-using-chrome-extension */ function hackAttachShadow () { if (window._hasHackAttachShadow_) return try { window._shadowDomList_ = []; window.Element.prototype._attachShadow = window.Element.prototype.attachShadow; window.Element.prototype.attachShadow = function () { const arg = arguments; if (arg[0] && arg[0].mode) { // 强制使用 open mode arg[0].mode = 'open'; } const shadowRoot = this._attachShadow.apply(this, arg); // 存一份shadowDomList window._shadowDomList_.push(shadowRoot); /* 让shadowRoot里面的元素有机会访问shadowHost */ shadowRoot._shadowHost = this; // 在document下面添加 addShadowRoot 自定义事件 const shadowEvent = new window.CustomEvent('addShadowRoot', { shadowRoot, detail: { shadowRoot, message: 'addShadowRoot', time: new Date() }, bubbles: true, cancelable: true }); document.dispatchEvent(shadowEvent); return shadowRoot }; window._hasHackAttachShadow_ = true; } catch (e) { console.error('hackAttachShadow error by h5player plug-in', e); } } /*! * @name original.js * @description 存储部分重要的原生函数,防止被外部污染,此逻辑应尽可能前置,否则存储的将是污染后的函数 * @version 0.0.1 * @author xxxily * @date 2022/10/16 10:32 * @github https://github.com/xxxily */ const original = { // 防止defineProperty和defineProperties被AOP脚本重写 Object: { defineProperty: Object.defineProperty, defineProperties: Object.defineProperties }, // 防止此类玩法:https://juejin.cn/post/6865910564817010702 Proxy, Map, map: { clear: Map.prototype.clear, set: Map.prototype.set, has: Map.prototype.has, get: Map.prototype.get, delete: Map.prototype.delete }, console: { log: console.log, info: console.info, error: console.error, warn: console.warn, table: console.table }, ShadowRoot, HTMLMediaElement, CustomEvent, // appendChild: Node.prototype.appendChild, JSON: { parse: JSON.parse, stringify: JSON.stringify }, alert, confirm, prompt }; /** * 媒体标签检测,可以检测出viode、audio、以及其它标签名经过改造后的媒体Element * @param {Function} handler -必选 检出后要执行的回调函数 * @returns mediaElementList */ const mediaCore = (function () { let hasMediaCoreInit = false; let hasProxyHTMLMediaElement = false; let originDescriptors = {}; const originMethods = {}; const mediaElementList = []; const mediaElementHandler = []; const mediaMap = new original.Map(); const firstUpperCase = str => str.replace(/^\S/, s => s.toUpperCase()); function isHTMLMediaElement (el) { return el instanceof original.HTMLMediaElement } /** * 根据HTMLMediaElement的实例对象创建增强控制的相关API函数,从而实现锁定播放倍速,锁定暂停和播放等增强功能 * @param {*} mediaElement - 必选,HTMLMediaElement的具体实例,例如网页上的video标签或new Audio()等 * @returns mediaPlusApi */ function createMediaPlusApi (mediaElement) { if (!isHTMLMediaElement(mediaElement)) { return false } let mediaPlusApi = original.map.get.call(mediaMap, mediaElement); if (mediaPlusApi) { return mediaPlusApi } /* 创建MediaPlusApi对象 */ mediaPlusApi = {}; const mediaPlusBaseApi = { /** * 创建锁,阻止外部逻辑操作mediaElement相关的属性或函数 * 这里的锁逻辑只是数据状态标注和切换,具体的锁功能需在 * proxyPrototypeMethod和hijackPrototypeProperty里实现 */ lock (keyName, duration) { const infoKey = `__${keyName}_info__`; mediaPlusApi[infoKey] = mediaPlusApi[infoKey] || {}; mediaPlusApi[infoKey].lock = true; /* 解锁时间信息 */ duration = Number(duration); if (!Number.isNaN(duration) && duration > 0) { mediaPlusApi[infoKey].unLockTime = Date.now() + duration; } // original.console.log(`[mediaPlusApi][lock][${keyName}] ${duration}`) }, unLock (keyName) { const infoKey = `__${keyName}_info__`; mediaPlusApi[infoKey] = mediaPlusApi[infoKey] || {}; mediaPlusApi[infoKey].lock = false; mediaPlusApi[infoKey].unLockTime = Date.now() - 100; // original.console.log(`[mediaPlusApi][unLock][${keyName}]`) }, isLock (keyName) { const info = mediaPlusApi[`__${keyName}_info__`] || {}; if (info.unLockTime) { /* 延时锁根据当前时间计算是否还处于锁状态 */ return Date.now() < info.unLockTime } else { return info.lock || false } }, /* 注意:调用此处的get和set和apply不受锁的限制 */ get (keyName) { if (originDescriptors[keyName] && originDescriptors[keyName].get && !originMethods[keyName]) { return originDescriptors[keyName].get.apply(mediaElement) } }, set (keyName, val) { if (originDescriptors[keyName] && originDescriptors[keyName].set && !originMethods[keyName] && typeof val !== 'undefined') { // original.console.log(`[mediaPlusApi][${keyName}] 执行原生set操作`) return originDescriptors[keyName].set.apply(mediaElement, [val]) } }, apply (keyName) { if (originMethods[keyName] instanceof Function) { const args = Array.from(arguments); args.shift(); // original.console.log(`[mediaPlusApi][${keyName}] 执行原生apply操作`) return originMethods[keyName].apply(mediaElement, args) } } }; mediaPlusApi = { ...mediaPlusApi, ...mediaPlusBaseApi }; /** * 扩展api列表。实现'playbackRate', 'volume', 'currentTime', 'play', 'pause'的纯api调用效果,具体可用API如下: * mediaPlusApi.lockPlaybackRate() * mediaPlusApi.unLockPlaybackRate() * mediaPlusApi.isLockPlaybackRate() * mediaPlusApi.getPlaybackRate() * mediaPlusApi.setPlaybackRate(val) * * mediaPlusApi.lockVolume() * mediaPlusApi.unLockVolume() * mediaPlusApi.isLockVolume() * mediaPlusApi.getVolume() * mediaPlusApi.setVolume(val) * * mediaPlusApi.lockCurrentTime() * mediaPlusApi.unLockCurrentTime() * mediaPlusApi.isLockCurrentTime() * mediaPlusApi.getCurrentTime() * mediaPlusApi.setCurrentTime(val) * * mediaPlusApi.lockPlay() * mediaPlusApi.unLockPlay() * mediaPlusApi.isLockPlay() * mediaPlusApi.applyPlay() * * mediaPlusApi.lockPause() * mediaPlusApi.unLockPause() * mediaPlusApi.isLockPause() * mediaPlusApi.applyPause() */ const extApiKeys = ['playbackRate', 'volume', 'currentTime', 'play', 'pause']; const baseApiKeys = Object.keys(mediaPlusBaseApi); extApiKeys.forEach(key => { baseApiKeys.forEach(baseKey => { /* 当key对应的是函数时,不应该有get、set的api,而应该有apply的api */ if (originMethods[key] instanceof Function) { if (baseKey === 'get' || baseKey === 'set') { return true } } else if (baseKey === 'apply') { return true } mediaPlusApi[`${baseKey}${firstUpperCase(key)}`] = function () { return mediaPlusBaseApi[baseKey].apply(null, [key, ...arguments]) }; }); }); original.map.set.call(mediaMap, mediaElement, mediaPlusApi); return mediaPlusApi } /* 检测到media对象的处理逻辑,依赖Proxy对media函数的代理 */ function mediaDetectHandler (ctx) { if (isHTMLMediaElement(ctx) && !mediaElementList.includes(ctx)) { // console.log(`[mediaDetectHandler]`, ctx) mediaElementList.push(ctx); createMediaPlusApi(ctx); try { mediaElementHandler.forEach(handler => { (handler instanceof Function) && handler(ctx); }); } catch (e) {} } } /* 代理方法play和pause方法,确保能正确暂停和播放 */ function proxyPrototypeMethod (element, methodName) { const originFunc = element && element.prototype[methodName]; if (!originFunc) return element.prototype[methodName] = new original.Proxy(originFunc, { apply (target, ctx, args) { mediaDetectHandler(ctx); // original.console.log(`[mediaElementMethodProxy] 执行代理后的${methodName}函数`) /* 对播放暂停逻辑进行增强处理,例如允许通过mediaPlusApi进行锁定 */ if (['play', 'pause'].includes(methodName)) { const mediaPlusApi = createMediaPlusApi(ctx); if (mediaPlusApi && mediaPlusApi.isLock(methodName)) { // original.console.log(`[mediaElementMethodProxy] ${methodName}已被锁定,无法执行相关操作`) return } } const result = target.apply(ctx, args); // TODO 对函数执行结果进行观察判断 return result } }); // 不建议对HTMLMediaElement的原型链进行扩展,这样容易让网页检测到mediaCore增强逻辑的存在 // if (originMethods[methodName]) { // element.prototype[`__${methodName}__`] = originMethods[methodName] // } } /** * 劫持 playbackRate、volume、currentTime 属性,并增加锁定的逻辑,从而实现更强的抗干扰能力 */ function hijackPrototypeProperty (element, property) { if (!element || !element.prototype || !originDescriptors[property]) { return false } original.Object.defineProperty.call(Object, element.prototype, property, { configurable: true, enumerable: true, get: function () { const val = originDescriptors[property].get.apply(this, arguments); // original.console.log(`[mediaElementPropertyHijack][${property}][get]`, val) const mediaPlusApi = createMediaPlusApi(this); if (mediaPlusApi && mediaPlusApi.isLock(property)) { if (property === 'playbackRate') { return +!+[] } } return val }, set: function (value) { // original.console.log(`[mediaElementPropertyHijack][${property}][set]`, value) if (property === 'src') { mediaDetectHandler(this); } /* 对调速、调音和进度控制逻辑进行增强处理,例如允许通过mediaPlusApi这些功能进行锁定 */ if (['playbackRate', 'volume', 'currentTime'].includes(property)) { const mediaPlusApi = createMediaPlusApi(this); if (mediaPlusApi && mediaPlusApi.isLock(property)) { // original.console.log(`[mediaElementPropertyHijack] ${property}已被锁定,无法执行相关操作`) return } } return originDescriptors[property].set.apply(this, arguments) } }); } function mediaPlus (mediaElement) { return createMediaPlusApi(mediaElement) } function mediaProxy () { if (!hasProxyHTMLMediaElement) { const proxyMethods = ['play', 'pause', 'load', 'addEventListener']; proxyMethods.forEach(methodName => { proxyPrototypeMethod(HTMLMediaElement, methodName); }); const hijackProperty = ['playbackRate', 'volume', 'currentTime', 'src']; hijackProperty.forEach(property => { hijackPrototypeProperty(HTMLMediaElement, property); }); hasProxyHTMLMediaElement = true; } return hasProxyHTMLMediaElement } /** * 媒体标签检测,可以检测出viode、audio、以及其它标签名经过改造后的媒体Element * @param {Function} handler -必选 检出后要执行的回调函数 * @returns mediaElementList */ function mediaChecker (handler) { if (!(handler instanceof Function) || mediaElementHandler.includes(handler)) { return mediaElementList } else { mediaElementHandler.push(handler); } if (!hasProxyHTMLMediaElement) { mediaProxy(); } return mediaElementList } /** * 初始化mediaCore相关功能 */ function init (mediaCheckerHandler) { if (hasMediaCoreInit) { return false } originDescriptors = Object.getOwnPropertyDescriptors(HTMLMediaElement.prototype); Object.keys(HTMLMediaElement.prototype).forEach(key => { try { if (HTMLMediaElement.prototype[key] instanceof Function) { originMethods[key] = HTMLMediaElement.prototype[key]; } } catch (e) {} }); mediaCheckerHandler = mediaCheckerHandler instanceof Function ? mediaCheckerHandler : function () {}; mediaChecker(mediaCheckerHandler); hasMediaCoreInit = true; return true } return { init, mediaPlus, mediaChecker, originDescriptors, originMethods, mediaElementList } })(); /*! * @name utils.js * @description 数据类型相关的方法 * @version 0.0.1 * @author Blaze * @date 22/03/2019 22:46 * @github https://github.com/xxxily */ /** * 准确地获取对象的具体类型 参见:https://www.talkingcoder.com/article/6333557442705696719 * @param obj { all } -必选 要判断的对象 * @returns {*} 返回判断的具体类型 */ function getType (obj) { if (obj == null) { return String(obj) } return typeof obj === 'object' || typeof obj === 'function' ? (obj.constructor && obj.constructor.name && obj.constructor.name.toLowerCase()) || /function\s(.+?)\(/.exec(obj.constructor)[1].toLowerCase() : typeof obj } const isType = (obj, typeName) => getType(obj) === typeName; const isObj$1 = obj => isType(obj, 'object'); /*! * @name object.js * @description 对象操作的相关方法 * @version 0.0.1 * @author Blaze * @date 21/03/2019 23:10 * @github https://github.com/xxxily */ /** * 对一个对象进行深度拷贝 * @source -必选(Object|Array)需拷贝的对象或数组 */ function clone (source) { var result = {}; if (typeof source !== 'object') { return source } if (Object.prototype.toString.call(source) === '[object Array]') { result = []; } if (Object.prototype.toString.call(source) === '[object Null]') { result = null; } for (var key in source) { result[key] = (typeof source[key] === 'object') ? clone(source[key]) : source[key]; } return result } /* 遍历对象,但不包含其原型链上的属性 */ function forIn (obj, fn) { fn = fn || function () {}; for (var key in obj) { if (Object.hasOwnProperty.call(obj, key)) { fn(key, obj[key]); } } } /** * 深度合并两个可枚举的对象 * @param objA {object} -必选 对象A * @param objB {object} -必选 对象B * @param concatArr {boolean} -可选 合并数组,默认遇到数组的时候,直接以另外一个数组替换当前数组,将此设置true则,遇到数组的时候一律合并,而不是直接替换 * @returns {*|void} */ function mergeObj (objA, objB, concatArr) { function isObj (obj) { return Object.prototype.toString.call(obj) === '[object Object]' } function isArr (arr) { return Object.prototype.toString.call(arr) === '[object Array]' } if (!isObj(objA) || !isObj(objB)) return objA function deepMerge (objA, objB) { forIn(objB, function (key) { const subItemA = objA[key]; const subItemB = objB[key]; if (typeof subItemA === 'undefined') { objA[key] = subItemB; } else { if (isObj(subItemA) && isObj(subItemB)) { /* 进行深层合并 */ objA[key] = deepMerge(subItemA, subItemB); } else { if (concatArr && isArr(subItemA) && isArr(subItemB)) { objA[key] = subItemA.concat(subItemB); } else { objA[key] = subItemB; } } } }); return objA } return deepMerge(objA, objB) } /** * 根据文本路径获取对象里面的值,如需支持数组请使用lodash的get方法 * @param obj {Object} -必选 要操作的对象 * @param path {String} -必选 路径信息 * @returns {*} */ function getValByPath$1 (obj, path) { path = path || ''; const pathArr = path.split('.'); let result = obj; /* 递归提取结果值 */ for (let i = 0; i < pathArr.length; i++) { if (!result) break result = result[pathArr[i]]; } return result } /** * 根据文本路径设置对象里面的值,如需支持数组请使用lodash的set方法 * @param obj {Object} -必选 要操作的对象 * @param path {String} -必选 路径信息 * @param val {Any} -必选 如果不传该参,最终结果会被设置为undefined * @returns {Boolean} 返回true表示设置成功,否则设置失败 */ function setValByPath (obj, path, val) { if (!obj || !path || typeof path !== 'string') { return false } let result = obj; const pathArr = path.split('.'); for (let i = 0; i < pathArr.length; i++) { if (!result) break if (i === pathArr.length - 1) { result[pathArr[i]] = val; return Number.isNaN(val) ? Number.isNaN(result[pathArr[i]]) : result[pathArr[i]] === val } result = result[pathArr[i]]; } return false } const quickSort = function (arr) { if (arr.length <= 1) { return arr } var pivotIndex = Math.floor(arr.length / 2); var pivot = arr.splice(pivotIndex, 1)[0]; var left = []; var right = []; for (var i = 0; i < arr.length; i++) { if (arr[i] < pivot) { left.push(arr[i]); } else { right.push(arr[i]); } } return quickSort(left).concat([pivot], quickSort(right)) }; function hideDom (selector, delay) { setTimeout(function () { const dom = document.querySelector(selector); if (dom) { dom.style.opacity = 0; } }, delay || 1000 * 5); } /** * 向上查找操作 * @param dom {Element} -必选 初始dom元素 * @param fn {function} -必选 每一级ParentNode的回调操作 * 如果函数返回true则表示停止向上查找动作 */ function eachParentNode (dom, fn) { let parent = dom.parentNode; while (parent) { const isEnd = fn(parent, dom); parent = parent.parentNode; if (isEnd) { break } } } /** * 动态加载css内容 * @param cssText {String} -必选 样式的文本内容 * @param id {String} -可选 指定样式文本的id号,如果已存在对应id号则不会再次插入 * @param insetTo {Dom} -可选 指定插入到哪 * @returns {HTMLStyleElement} */ function loadCSSText (cssText, id, insetTo) { if (id && document.getElementById(id)) { return false } const style = document.createElement('style'); const head = insetTo || document.head || document.getElementsByTagName('head')[0]; style.appendChild(document.createTextNode(cssText)); head.appendChild(style); if (id) { style.setAttribute('id', id); } return style } /** * 判断当前元素是否为可编辑元素 * @param target * @returns Boolean */ function isEditableTarget (target) { const isEditable = target.getAttribute && target.getAttribute('contenteditable') === 'true'; const isInputDom = /INPUT|TEXTAREA|SELECT|LABEL/.test(target.nodeName); return isEditable || isInputDom } /** * 判断某个元素是否处于shadowDom里面 * 参考:https://www.coder.work/article/299700 * @param node * @returns {boolean} */ function isInShadow (node, returnShadowRoot) { for (; node; node = node.parentNode) { if (node.toString() === '[object ShadowRoot]') { if (returnShadowRoot) { return node } else { return true } } } return false } /** * 判断某个元素是否处于可视区域,适用于被动调用情况,需要高性能,请使用IntersectionObserver * 参考:https://github.com/febobo/web-interview/issues/84 * @param element * @returns {boolean} */ function isInViewPort (element) { const viewWidth = window.innerWidth || document.documentElement.clientWidth; const viewHeight = window.innerHeight || document.documentElement.clientHeight; const { top, right, bottom, left } = element.getBoundingClientRect(); return ( top >= 0 && left >= 0 && right <= viewWidth && bottom <= viewHeight ) } /** * 基于IntersectionObserver的可视区域判断 * @param { Function } callback * @param { Element } element * @returns { IntersectionObserver } */ function observeVisibility (callback, element) { const observer = new IntersectionObserver((entries, observer) => { entries.forEach(entry => { if (entry.isIntersecting) { /* 元素在可视区域内 */ callback(entry, observer); } else { /* 元素不在可视区域内 */ callback(null, observer); } }); }); if (element) { observer.observe(element); } /* 返回观察对象,以便外部可以取消观察:observer.disconnect(),或者增加新的观察对象:observer.observe(element) */ return observer } // 使用示例: // const temp1 = document.querySelector('#temp1') // var observer = observeVisibility(function (entry, observer) { // if (entry) { // console.log('[entry]', entry) // } else { // console.log('[entry]', 'null') // } // }, temp1) /** * 判断是否为不可见的元素,主要用以判断是否已经脱离文档流或被设置为display:none的元素 * @param {*} element * @returns */ function isOutOfDocument (element) { if (!element || element.offsetParent === null) { return true } const { top, right, bottom, left, width, height } = element.getBoundingClientRect(); return ( top === 0 && right === 0 && bottom === 0 && left === 0 && width === 0 && height === 0 ) } /** * 有些网站开启了CSP,会导致无法使用innerHTML,所以需要使用trustedTypes * https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/trusted-types * @param { String } htmlString -必选 HTML字符串 * @returns */ function createTrustedHTML (htmlString) { if (window.trustedTypes && window.trustedTypes.createPolicy) { /* 创建default策略前先检查是否已经存在 */ let policy = window.trustedTypes.defaultPolicy || null; if (!policy) { policy = window.trustedTypes.createPolicy('default', { createHTML: (string) => string }); } const trustedHTML = policy.createHTML(htmlString); return trustedHTML } else { return htmlString } } /** * 解析HTML字符串,返回DOM节点数组 * @param { String } -必选 htmlString HTML字符串 * @param { HTMLElement } -可选 targetElement 目标元素,如果传入,则会将解析后的节点添加到该元素中 * @returns { Array } DOM节点数组 */ function parseHTML (htmlString, targetElement) { if (typeof htmlString !== 'string') { throw new Error('[parseHTML] Input must be a string') } const trustedHTML = createTrustedHTML(htmlString); const parser = new DOMParser(); const doc = parser.parseFromString(trustedHTML, 'text/html'); const nodes = doc.body.childNodes; const result = []; if (targetElement && targetElement.appendChild) { nodes.forEach(node => { const targetNode = node.cloneNode(true); try { /* 有些网站出于业务需要会对appendChild进行重写,可能会导致appendChild报错,所以这里需要try catch */ targetElement.appendChild(targetNode); } catch (e) { console.error('[parseHTML] appendChild error', e, targetElement, targetNode); } result.push(targetNode); }); } return result.length ? result : nodes } /** * 将行内样式转换成对象的形式 * @param {string} inlineStyle -必选,例如: position: relative; opacity: 1; visibility: hidden; transform: scale(0.1) rotate(180deg); * @returns {Object} */ function inlineStyleToObj (inlineStyle) { if (typeof inlineStyle !== 'string') { return {} } const result = {}; const styArr = inlineStyle.split(';'); styArr.forEach(item => { const tmpArr = item.split(':'); if (tmpArr.length === 2) { result[tmpArr[0].trim()] = tmpArr[1].trim(); } }); return result } function objToInlineStyle (obj) { if (Object.prototype.toString.call(obj) !== '[object Object]') { return '' } const styleArr = []; Object.keys(obj).forEach(key => { styleArr.push(`${key}: ${obj[key]}`); }); return styleArr.join('; ') } /* ua信息伪装 */ function fakeUA (ua) { // Object.defineProperty(navigator, 'userAgent', { // value: ua, // writable: false, // configurable: false, // enumerable: true // }) const desc = Object.getOwnPropertyDescriptor(Navigator.prototype, 'userAgent'); Object.defineProperty(Navigator.prototype, 'userAgent', { ...desc, get: function () { return ua } }); } /* ua信息来源:https://developers.whatismybrowser.com */ const userAgentMap = { android: { chrome: 'Mozilla/5.0 (Linux; Android 9; SM-G960F Build/PPR1.180610.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/74.0.3729.157 Mobile Safari/537.36', firefox: 'Mozilla/5.0 (Android 7.0; Mobile; rv:57.0) Gecko/57.0 Firefox/57.0' }, iPhone: { safari: 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/111.0.0.0 Mobile/15E148 Safari/604.1', chrome: 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/74.0.3729.121 Mobile/15E148 Safari/605.1' }, iPad: { safari: 'Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1 Mobile/15E148 Safari/604.1', chrome: 'Mozilla/5.0 (iPad; CPU OS 12_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/74.0.3729.155 Mobile/15E148 Safari/605.1' }, mac: { safari: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.1 Safari/605.1.15', chrome: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Firefox) Chrome/74.0.3729.157 Safari/537.36' } }; /** * 判断是否处于Iframe中 * @returns {boolean} */ function isInIframe () { return window !== window.top } /** * 判断是否处于跨域限制的Iframe中 * @returns {boolean} */ function isInCrossOriginFrame () { let result = true; try { if (window.top.localStorage || window.top.location.href) { result = false; } } catch (e) { result = true; } return result } /** * 简单的节流函数 * @param fn * @param interval * @returns {Function} */ function throttle (fn, interval = 80) { let timeout = null; return function () { if (timeout) return false timeout = setTimeout(() => { timeout = null; }, interval); fn.apply(this, arguments); } } /*! * @name url.js * @description 用于对url进行解析的相关方法 * @version 0.0.1 * @author Blaze * @date 27/03/2019 15:52 * @github https://github.com/xxxily */ /** * 参考示例: * https://segmentfault.com/a/1190000006215495 * 注意:该方法必须依赖浏览器的DOM对象 */ function parseURL (url) { var a = document.createElement('a'); a.href = url || window.location.href; return { source: url, protocol: a.protocol.replace(':', ''), host: a.hostname, port: a.port, origin: a.origin, search: a.search, query: a.search, file: (a.pathname.match(/\/([^/?#]+)$/i) || ['', ''])[1], hash: a.hash.replace('#', ''), path: a.pathname.replace(/^([^/])/, '/$1'), relative: (a.href.match(/tps?:\/\/[^/]+(.+)/) || ['', ''])[1], params: (function () { var ret = {}; var seg = []; var paramArr = a.search.replace(/^\?/, '').split('&'); for (var i = 0; i < paramArr.length; i++) { var item = paramArr[i]; if (item !== '' && item.indexOf('=')) { seg.push(item); } } for (var j = 0; j < seg.length; j++) { var param = seg[j]; var idx = param.indexOf('='); var key = param.substring(0, idx); var val = param.substring(idx + 1); if (!key) { ret[val] = null; } else { ret[key] = val; } } return ret })() } } /** * 将params对象转换成字符串模式 * @param params {Object} - 必选 params对象 * @returns {string} */ function stringifyParams (params) { var strArr = []; if (!Object.prototype.toString.call(params) === '[object Object]') { return '' } for (var key in params) { if (Object.hasOwnProperty.call(params, key)) { var val = params[key]; var valType = Object.prototype.toString.call(val); if (val === '' || valType === '[object Undefined]') continue if (val === null) { strArr.push(key); } else if (valType === '[object Array]') { strArr.push(key + '=' + val.join(',')); } else { val = (JSON.stringify(val) || '' + val).replace(/(^"|"$)/g, ''); strArr.push(key + '=' + val); } } } return strArr.join('&') } /** * 将通过parseURL解析出来url对象重新还原成url地址 * 主要用于查询参数被动态修改后,再重组url链接 * @param obj {Object} -必选 parseURL解析出来url对象 */ function stringifyToUrl (urlObj) { var query = stringifyParams(urlObj.params) || ''; if (query) { query = '?' + query; } var hash = urlObj.hash ? '#' + urlObj.hash : ''; return urlObj.origin + urlObj.path + query + hash } /* 当前用到的快捷键 */ const hasUseKey = { keyCodeList: [13, 16, 17, 18, 27, 32, 37, 38, 39, 40, 49, 50, 51, 52, 67, 68, 69, 70, 73, 74, 75, 77, 78, 79, 80, 81, 82, 83, 84, 85, 87, 88, 89, 90, 97, 98, 99, 100, 220], keyList: ['enter', 'shift', 'control', 'alt', 'escape', ' ', 'arrowleft', 'arrowright', 'arrowup', 'arrowdown', '1', '2', '3', '4', 'c', 'd', 'e', 'f', 'i', 'j', 'k', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'w', 'x', 'y', 'z', '\\', '|'], keyMap: { enter: 13, shift: 16, ctrl: 17, alt: 18, esc: 27, space: 32, '←': 37, '↑': 38, '→': 39, '↓': 40, 1: 49, 2: 50, 3: 51, 4: 52, c: 67, d: 68, e: 69, f: 70, i: 73, j: 74, k: 75, m: 77, n: 78, o: 79, p: 80, q: 81, r: 82, s: 83, t: 84, u: 85, w: 87, x: 88, y: 89, z: 90, pad1: 97, pad2: 98, pad3: 99, pad4: 100, '\\': 220 } }; /** * 判断当前按键是否注册为需要用的按键 * 用于减少对其它键位的干扰 */ function isRegisterKey (event) { const keyCode = event.keyCode; const key = event.key.toLowerCase(); return hasUseKey.keyCodeList.includes(keyCode) || hasUseKey.keyList.includes(key) } /** * 由于tampermonkey对window对象进行了封装,我们实际访问到的window并非页面真实的window * 这就导致了如果我们需要将某些对象挂载到页面的window进行调试的时候就无法挂载了 * 所以必须使用特殊手段才能访问到页面真实的window对象,于是就有了下面这个函数 * @returns {Promise} */ async function getPageWindow () { return new Promise(function (resolve, reject) { if (window._pageWindow) { return resolve(window._pageWindow) } /* 尝试通过同步的方式获取pageWindow */ try { const pageWin = getPageWindowSync(); if (pageWin && pageWin.document && pageWin.XMLHttpRequest) { window._pageWindow = pageWin; resolve(pageWin); return pageWin } } catch (e) {} /* 下面异步获取pagewindow的方法在最新的chrome浏览器里已失效 */ const listenEventList = ['load', 'mousemove', 'scroll', 'get-page-window-event']; function getWin (event) { window._pageWindow = this; // debug.log('getPageWindow succeed', event) listenEventList.forEach(eventType => { window.removeEventListener(eventType, getWin, true); }); resolve(window._pageWindow); } listenEventList.forEach(eventType => { window.addEventListener(eventType, getWin, true); }); /* 自行派发事件以便用最短的时间获得pageWindow对象 */ window.dispatchEvent(new window.Event('get-page-window-event')); }) } getPageWindow(); /** * 通过同步的方式获取pageWindow * 注意同步获取的方式需要将脚本写入head,部分网站由于安全策略会导致写入失败,而无法正常获取 * @returns {*} */ function getPageWindowSync (rawFunction) { if (window.unsafeWindow) return window.unsafeWindow if (document._win_) return document._win_ try { rawFunction = rawFunction || window.__rawFunction__ || Function.prototype.constructor; // return rawFunction('return window')() // Function('return (function(){}.constructor("return this")());') return rawFunction('return (function(){}.constructor("var getPageWindowSync=1; return this")());')() } catch (e) { console.error('getPageWindowSync error', e); const head = document.head || document.querySelector('head'); const script = document.createElement('script'); script.appendChild(document.createTextNode('document._win_ = window')); head.appendChild(script); return document._win_ } } function openInTab (url, opts, referer) { if (referer) { const urlObj = parseURL(url); if (!urlObj.params.referer) { urlObj.params.referer = encodeURIComponent(window.location.href); url = stringifyToUrl(urlObj); } } if (window.GM_openInTab) { window.GM_openInTab(url, opts || { active: true, insert: true, setParent: true }); } else { // 创建新的a标签并模拟点击 const a = document.createElement('a'); a.href = url; a.target = '_blank'; a.rel = 'noopener noreferrer'; a.style.display = 'inline-block'; a.style.width = '1px'; a.style.height = '1px'; a.style.opcity = 0; document.body.appendChild(a); a.click(); setTimeout(() => { document.body.removeChild(a); }, 300); } } /* 确保数字为正数 */ function numUp (num) { if (typeof num === 'number' && num < 0) { num = Math.abs(num); } return num } /* 确保数字为负数 */ function numDown (num) { if (typeof num === 'number' && num > 0) { num = -num; } return num } function isMediaElement (element) { return element && (element instanceof HTMLMediaElement || element.HTMLMediaElement || element.HTMLVideoElement || element.HTMLAudioElement) } function isVideoElement (element) { return element && (element instanceof HTMLVideoElement || element.HTMLVideoElement) } function isAudioElement (element) { return element && (element instanceof HTMLAudioElement || element.HTMLAudioElement) } /*! * configManager parse localStorage error * @name configManager.ts * @description 配置统一管理脚本 * @version 0.0.1 * @author xxxily * @date 2023/03/06 14:29 * @github https://github.com/xxxily */ /** * 判断localStorage是否可用 * localStorage并不能保证100%可用,所以使用前必须进行判断,否则会导致部分网站下脚本出现异常 * https://stackoverflow.com/questions/30481516/iframe-in-chrome-error-failed-to-read-localstorage-from-window-access-deni * https://cloud.tencent.com/developer/article/1803097 (当localStorage不能用时,window.localStorage为null,而不是文中的undefined) */ function isLocalStorageUsable () { return window.localStorage && window.localStorage.getItem instanceof Function && window.localStorage.setItem instanceof Function } /** * 判断GlobalStorage是否可用,目前使用的GlobalStorage是基于tampermonkey提供的相关api * https://www.tampermonkey.net/documentation.php?ext=dhdg#GM_setValue */ function isGlobalStorageUsable () { return window.GM_setValue && window.GM_getValue && window.GM_deleteValue && window.GM_listValues instanceof Function } /** * 存储干净的localStorage相关方法 * 防止localStorage对象下的方法被改写而导致读取和写入规则不一样的问题 */ const rawLocalStorage = (function getRawLocalStorage () { const localStorageApis = ['getItem', 'setItem', 'removeItem', 'clear', 'key']; const rawLocalStorage = {}; localStorageApis.forEach((apiKey) => { if (isLocalStorageUsable()) { rawLocalStorage[`_${apiKey}_`] = localStorage[apiKey]; rawLocalStorage[apiKey] = function () { return rawLocalStorage[`_${apiKey}_`].apply(localStorage, arguments) }; } else { rawLocalStorage[apiKey] = function () { console.error('localStorage unavailable'); }; } }); return rawLocalStorage })(); class ConfigManager { constructor (opts) { this.opts = opts; } isLocalStorageUsable = isLocalStorageUsable isGlobalStorageUsable = isGlobalStorageUsable /** * 将confPath转换称最终存储到localStorage或globalStorage里的键名 * @param {String} confPath -必选,配置路径信息:例如:'enhance.blockSetPlaybackRate' * @returns {keyName} */ getConfKeyName (confPath = '') { return this.opts.prefix + confPath.replace(/\./g, '_') } /** * 将存储到localStorage或globalStorage里的键名转换成实际调用时候的confPath * @param {String} keyName -必选 存储到localStorage或globalStorage里的键名,例如:'_h5player_enhance_blockSetPlaybackRate' * @returns {confPath} */ getConfPath (keyName = '') { return keyName.replace(this.opts.prefix, '').replace(/_/g, '.') } getConfPathList (config) { const confPathList = []; /* 递归获取所有配置项的路径 */ function getConfPathList (config, path = '') { Object.keys(config).forEach((key) => { const pathKey = path ? `${path}.${key}` : key; if (Object.prototype.toString.call(config[key]) === '[object Object]') { getConfPathList(config[key], pathKey); } else { confPathList.push(pathKey); } }); } getConfPathList(config); return confPathList } /** * 根据给定的配置路径,获取相关配置信息 * 获取顺序:LocalStorage > GlobalStorage > defConfig > null * @param {String} confPath -必选,配置路径信息:例如:'enhance.blockSetPlaybackRate' * @returns {*} 如果返回null,则表示没获取到相关配置信息 */ get (confPath) { if (typeof confPath !== 'string') { return null } /* 默认优先使用本地的localStorage配置 */ const localConf = this.getLocalStorage(confPath); if (localConf !== null && localConf !== undefined) { return localConf } /* 如果localStorage没相关配置,则尝试使用GlobalStorage的配置 */ const globalConf = this.getGlobalStorage(confPath); if (globalConf !== null && globalConf !== undefined) { return globalConf } /* 如果localStorage和GlobalStorage配置都没找到,则尝试在默认配置表里拿相关配置信息 */ return this.getMemoryStorage(confPath) } /** * 将配置结果写入到localStorage或GlobalStorage * 写入顺序:LocalStorage > GlobalStorage * 无论是否写入成功都会将结果更新到defConfig里对应的配置项上 * @param {String} confPath * @param {*} val * @returns {Boolean} */ set (confPath, val) { if (typeof confPath !== 'string' || typeof val === 'undefined' || val === null) { return false } setValByPath(this.opts.config, confPath, val); let sucStatus = false; sucStatus = this.setLocalStorage(confPath, val); if (!sucStatus) { sucStatus = this.setGlobalStorage(confPath, val); } return sucStatus } /* 获取并列出当前所有已设定的配置项 */ list () { const result = { localConf: this.listLocalStorage(), globalConf: this.listGlobalStorage(), defConfig: this.opts.config }; return result } /* 清除已经写入到本地存储里的配置项 */ clear () { this.clearLocalStorage(); this.clearGlobalStorage(); } getMemoryStorage (confPath) { if (typeof confPath !== 'string') { return null } const config = this.getConfObj(); const val = getValByPath$1(config, confPath); if (typeof val !== 'undefined' && val !== null) { return val } else { return null } } /** * 根据给定的配置路径,获取LocalStorage下定义的配置信息 * @param {String} confPath -必选,配置路径信息 * @returns */ getLocalStorage (confPath) { if (typeof confPath !== 'string') { return null } const key = this.getConfKeyName(confPath); if (isLocalStorageUsable()) { let localConf = rawLocalStorage.getItem(key); if (localConf !== null && localConf !== undefined) { try { localConf = JSON.parse(localConf); } catch (e) { console.error('configManager parse localStorage error:', key, localConf); } return localConf } else { return this.getMemoryStorage(confPath) } } return null } /** * 根据给定的配置路径,获取GlobalStorage下定义的配置信息 * @param {String} confPath -必选,配置路径信息 * @returns */ getGlobalStorage (confPath) { if (typeof confPath !== 'string') { return null } const key = this.getConfKeyName(confPath); if (isGlobalStorageUsable()) { const globalConf = window.GM_getValue(key); if (globalConf !== null && globalConf !== undefined) { return globalConf } else { return this.getMemoryStorage(confPath) } } else { /* 非油猴环境,回退到localStorage存储 */ return this.getLocalStorage(confPath) } } setMemoryStorage (confPath, val) { if (typeof confPath !== 'string' || typeof val === 'undefined' || val === null) { return false } else { setValByPath(this.opts.config, confPath, val); return true } } /** * 将配置结果写入到localStorage里 * @param {String} confPath * @param {*} val * @returns {Boolean} */ setLocalStorage (confPath, val) { if (typeof confPath !== 'string' || typeof val === 'undefined' || val === null) { return false } setValByPath(this.opts.config, confPath, val); const key = this.getConfKeyName(confPath); if (isLocalStorageUsable()) { try { if (Object.prototype.toString.call(val) === '[object Object]' || Array.isArray(val)) { val = JSON.stringify(val); } rawLocalStorage.setItem(key, val); return true } catch (e) { console.error('configManager set localStorage error:', key, val, e); return false } } else { return false } } /** * 将配置结果写入到globalStorage里 * @param {String} confPath * @param {*} val * @returns {Boolean} */ setGlobalStorage (confPath, val) { if (typeof confPath !== 'string' || typeof val === 'undefined' || val === null) { return false } setValByPath(this.opts.config, confPath, val); const key = this.getConfKeyName(confPath); if (isGlobalStorageUsable()) { try { window.GM_setValue(key, val); return true } catch (e) { console.error('configManager set globalStorage error:', key, val, e); return false } } else { /* 非油猴环境,回退到localStorage存储 */ return this.setLocalStorage(confPath, val) } } listLocalStorage () { if (isLocalStorageUsable()) { const result = {}; Object.keys(localStorage).forEach((key) => { if (key.startsWith(this.opts.prefix)) { const confPath = this.getConfPath(key); result[confPath] = this.getLocalStorage(confPath); } }); return result } else { return {} } } listGlobalStorage () { if (isGlobalStorageUsable()) { const result = {}; const globalStorage = window.GM_listValues(); globalStorage.forEach((key) => { if (key.startsWith(this.opts.prefix)) { const confPath = this.getConfPath(key); result[confPath] = this.getGlobalStorage(confPath); } }); return result } else { return {} } } getConfObj () { const confList = this.list(); /* 同步全局配置到this.opts.config */ Object.keys(confList.globalConf).forEach((confPath) => { setValByPath(this.opts.config, confPath, confList.globalConf[confPath]); }); /* 同步本地配置到this.opts.config */ Object.keys(confList.localConf).forEach((confPath) => { setValByPath(this.opts.config, confPath, confList.localConf[confPath]); }); return this.opts.config } setLocalStorageByObj (config) { const oldConfig = this.getConfObj(); const confPathList = this.getConfPathList(config); confPathList.forEach((confPath) => { const oldVal = getValByPath$1(oldConfig, confPath); const val = getValByPath$1(config, confPath); /* 跳过一样的值或在旧配置中不存在的值 */ if (oldVal === val || oldVal === undefined) { return } this.setLocalStorage(confPath, val); }); } setGlobalStorageByObj (config) { const oldConfig = this.getConfObj(); const confPathList = this.getConfPathList(config); confPathList.forEach((confPath) => { const oldVal = getValByPath$1(oldConfig, confPath); const val = getValByPath$1(config, confPath); /* 跳过一样的值或在旧配置中不存在的值 */ if (oldVal === val || oldVal === undefined) { return } // console.log('setGlobalStorageByObj', confPath, val) this.setGlobalStorage(confPath, val); }); } clearLocalStorage () { if (isLocalStorageUsable()) { Object.keys(localStorage).forEach((key) => { if (key.startsWith(this.opts.prefix)) { rawLocalStorage.removeItem(key); } }); } } clearGlobalStorage () { if (isGlobalStorageUsable()) { const globalStorage = window.GM_listValues(); globalStorage.forEach((key) => { if (key.startsWith(this.opts.prefix)) { window.GM_deleteValue(key); } }); } } mergeDefConf (conf) { return mergeObj(this.opts.config, conf) } } /* 使用示例: */ // const myConfig = new ConfigManager({ // prefix: '_myConfig_', // config: { // hotkeys: [ // { // desc: '测试', // key: 'v', // command: 'toggleVisible', // /* 如需禁用快捷键,将disabled设为true */ // disabled: false, // }, // ], // enable: true, // debug: false, // }, // }) // myConfig.set('enable', false) // /* 对于数组,暂不支持直接修改数组元素,需要先获取数组,再修改数组元素,再重新写入 */ // const hotkeys = myConfig.get('hotkeys') // hotkeys[0].disabled = true // myConfig.set('hotkeys', hotkeys) const configManager = new ConfigManager({ prefix: '_h5player_', config: { enable: true, media: { autoPlay: false, playbackRate: 1, volume: 1, /* 最后一次设定的播放速度,默认1.5 */ lastPlaybackRate: 1.5, /* 是否允许存储播放进度 */ allowRestorePlayProgress: { }, /* 视频播放进度映射表 */ progress: {} }, enableHotkeys: true, hotkeys: [ { desc: '网页全屏', key: 'shift+enter', command: 'setWebFullScreen', /* 如需禁用快捷键,将disabled设为true */ disabled: false }, { desc: '全屏', key: 'enter', command: 'setFullScreen' }, { desc: '切换画中画模式', key: 'shift+p', command: 'togglePictureInPicture' }, { desc: '视频截图', key: 'shift+s', command: 'capture' }, { desc: '启用或禁止自动恢复播放进度功能', key: 'shift+r', command: 'switchRestorePlayProgressStatus' }, { desc: '垂直镜像翻转', key: 'shift+m', command: 'setMirror', args: [true] }, { desc: '水平镜像翻转', key: 'm', command: 'setMirror' }, { desc: '下载音视频文件(实验性功能)', key: 'shift+d', command: 'mediaDownload' }, { desc: '缩小视频画面 -0.05', key: 'shift+x', command: 'setScaleDown' }, { desc: '放大视频画面 +0.05', key: 'shift+c', command: 'setScaleUp' }, { desc: '恢复视频画面', key: 'shift+z', command: 'resetTransform' }, { desc: '画面向右移动10px', key: 'shift+arrowright', command: 'setTranslateRight' }, { desc: '画面向左移动10px', key: 'shift+arrowleft', command: 'setTranslateLeft' }, { desc: '画面向上移动10px', key: 'shift+arrowup', command: 'setTranslateUp' }, { desc: '画面向下移动10px', key: 'shift+arrowdown', command: 'setTranslateDown' }, { desc: '前进5秒', key: 'arrowright', command: 'setCurrentTimeUp' }, { desc: '后退5秒', key: 'arrowleft', command: 'setCurrentTimeDown' }, { desc: '前进30秒', key: 'ctrl+arrowright', command: 'setCurrentTimeUp', args: [30] }, { desc: '后退30秒', key: 'ctrl+arrowleft', command: 'setCurrentTimeDown', args: [-30] }, { desc: '音量升高 5%', key: 'arrowup', command: 'setVolumeUp', args: [0.05] }, { desc: '音量降低 5%', key: 'arrowdown', command: 'setVolumeDown', args: [-0.05] }, { desc: '音量升高 20%', key: 'ctrl+arrowup', command: 'setVolumeUp', args: [0.2] }, { desc: '音量降低 20%', key: 'ctrl+arrowdown', command: 'setVolumeDown', args: [-0.2] }, { desc: '切换暂停/播放', key: 'space', command: 'switchPlayStatus' }, { desc: '减速播放 -0.1', key: 'x', command: 'setPlaybackRateDown' }, { desc: '加速播放 +0.1', key: 'c', command: 'setPlaybackRateUp' }, { desc: '正常速度播放', key: 'z', command: 'resetPlaybackRate' }, { desc: '设置1x的播放速度', key: 'Digit1', command: 'setPlaybackRatePlus', args: 1 }, { desc: '设置1x的播放速度', key: 'Numpad1', command: 'setPlaybackRatePlus', args: 1 }, { desc: '设置2x的播放速度', key: 'Digit2', command: 'setPlaybackRatePlus', args: 2 }, { desc: '设置2x的播放速度', key: 'Numpad2', command: 'setPlaybackRatePlus', args: 2 }, { desc: '设置3x的播放速度', key: 'Digit3', command: 'setPlaybackRatePlus', args: 3 }, { desc: '设置3x的播放速度', key: 'Numpad3', command: 'setPlaybackRatePlus', args: 3 }, { desc: '设置4x的播放速度', key: 'Digit4', command: 'setPlaybackRatePlus', args: 4 }, { desc: '设置4x的播放速度', key: 'Numpad4', command: 'setPlaybackRatePlus', args: 4 }, { desc: '下一帧', key: 'F', command: 'freezeFrame', args: 1 }, { desc: '上一帧', key: 'D', command: 'freezeFrame', args: -1 }, { desc: '增加亮度', key: 'E', command: 'setBrightnessUp' }, { desc: '减少亮度', key: 'W', command: 'setBrightnessDown' }, { desc: '增加对比度', key: 'T', command: 'setContrastUp' }, { desc: '减少对比度', key: 'R', command: 'setContrastDown' }, { desc: '增加饱和度', key: 'U', command: 'setSaturationUp' }, { desc: '减少饱和度', key: 'Y', command: 'setSaturationDown' }, { desc: '增加色相', key: 'O', command: 'setHueUp' }, { desc: '减少色相', key: 'I', command: 'setHueDown' }, { desc: '模糊增加 1 px', key: 'K', command: 'setBlurUp' }, { desc: '模糊减少 1 px', key: 'J', command: 'setBlurDown' }, { desc: '图像复位', key: 'Q', command: 'resetFilterAndTransform' }, { desc: '画面旋转 90 度', key: 'S', command: 'setRotate' }, { desc: '播放下一集', key: 'N', command: 'setNextVideo' }, { desc: '插入debugger断点', key: 'ctrl+shift+alt+d', command: 'debuggerNow' }, { desc: '执行JS脚本', key: 'ctrl+j ctrl+s', command: () => { alert('自定义JS脚本'); }, when: '' } ], ui: { enable: true, alwaysShow: false }, enhance: { /* 不禁用默认的调速逻辑,则在多个视频切换时,速度很容易被重置,所以该选项默认开启 */ blockSetPlaybackRate: true, blockSetCurrentTime: false, blockSetVolume: false, allowExperimentFeatures: false, allowExternalCustomConfiguration: false, /* 是否开启音量增益功能 */ allowAcousticGain: false, /* 是否开启跨域控制 */ allowCrossOriginControl: true, unfoldMenu: false }, language: 'auto', debug: false, /** * url黑名单,在这些url下面禁止运行h5player脚本 * 以适应一些难以排查、或难以通一兼容的页面,但又不希望对整个网站进行禁用的情况 * 例如:B站首页 */ blackUrlList: [ 'https://www.bilibili.com/' ] } }); async function initUiConfigManager () { const isUiConfigPage = location.href.indexOf('h5player.anzz.top/tools/json-editor') > -1; const isUiConfigMode = location.href.indexOf('saveHandlerName=saveH5PlayerConfig') > -1; if (!isUiConfigPage || !isUiConfigMode) return function init (pageWindow) { const config = JSON.parse(JSON.stringify(configManager.getConfObj())); delete config.recommendList; if (Array.isArray(config.hotkeys)) { /* 给hotkeys的各自项添加disabled选项,以便在界面侧可以快速禁用或启用某个项 */ config.hotkeys.forEach(item => { if (item.disabled === undefined) { item.disabled = false; } }); } pageWindow.jsonEditor.set(config); // pageWindow.jsonEditor.collapseAll() pageWindow.jsonEditor.expandAll(); pageWindow.saveH5PlayerConfig = function (editor) { try { const defConfig = configManager.getConfObj(); const newConfig = editor.get(); newConfig.recommendList = defConfig.recommendList || []; configManager.setGlobalStorageByObj(newConfig); alert('配置已更新'); } catch (e) { alert(`配置格式异常,保存失败:${e}`); } }; } let checkCount = 0; function checkJSONEditor (pageWindow) { if (!pageWindow.JSONEditor) { if (checkCount < 30) { setTimeout(() => { checkCount++; checkJSONEditor(pageWindow); }, 200); } return } init(pageWindow); } const pageWindow = await getPageWindow(); if (!pageWindow) { return } checkJSONEditor(pageWindow); } initUiConfigManager(); /** * 任务配置中心 Task Control Center * 用于配置所有无法进行通用处理的任务,如不同网站的全屏方式不一样,必须调用网站本身的全屏逻辑,才能确保字幕、弹幕等正常工作 **/ let TCC$1 = class TCC { constructor (taskConf, doTaskFunc) { this.conf = taskConf || { /** * 配置示例 * 父级键名对应的是一级域名, * 子级键名对应的相关功能名称,键值对应的该功能要触发的点击选择器或者要调用的相关函数 * 所有子级的键值都支持使用选择器触发或函数调用 * 配置了子级的则使用子级配置逻辑进行操作,否则使用默认逻辑 * 注意:include,exclude这两个子级键名除外,这两个是用来进行url范围匹配的 * */ 'demo.demo': { fullScreen: '.fullscreen-btn', exitFullScreen: '.exit-fullscreen-btn', webFullScreen: function () {}, exitWebFullScreen: '.exit-fullscreen-btn', autoPlay: '.player-start-btn', pause: '.player-pause', play: '.player-play', switchPlayStatus: '.player-play', playbackRate: function () {}, currentTime: function () {}, addCurrentTime: '.add-currenttime', subtractCurrentTime: '.subtract-currenttime', // 自定义快捷键的执行方式,如果是组合键,必须是 ctrl-->shift-->alt 这样的顺序,没有可以忽略,键名必须全小写 shortcuts: { /* 注册要执行自定义回调操作的快捷键 */ register: [ 'ctrl+shift+alt+c', 'ctrl+shift+c', 'ctrl+alt+c', 'ctrl+c', 'c' ], /* 自定义快捷键的回调操作 */ callback: function (h5Player, taskConf, data) { const { event, player } = data; console.log(event, player); } }, /* 当前域名下需包含的路径信息,默认整个域名下所有路径可用 必须是正则 */ include: /^.*/, /* 当前域名下需排除的路径信息,默认不排除任何路径 必须是正则 */ exclude: /\t/ } }; // 通过doTaskFunc回调定义配置该如何执行任务 this.doTaskFunc = doTaskFunc instanceof Function ? doTaskFunc : function () {}; } setTaskConf (taskConf) { this.conf = taskConf; } /** * 获取域名 , 目前实现方式不好,需改造,对地区性域名(如com.cn)、三级及以上域名支持不好 * */ getDomain () { const host = window.location.host; let domain = host; const tmpArr = host.split('.'); if (tmpArr.length > 2) { tmpArr.shift(); domain = tmpArr.join('.'); } return domain } /** * 格式化配置任务 * @param isAll { boolean } -可选 默认只格式当前域名或host下的配置任务,传入true则将所有域名下的任务配置都进行格式化 */ formatTCC (isAll) { const t = this; const keys = Object.keys(t.conf); const domain = t.getDomain(); const host = window.location.host; function formatter (item) { const defObj = { include: /^.*/, exclude: /\t/ }; item.include = item.include || defObj.include; item.exclude = item.exclude || defObj.exclude; return item } const result = {}; keys.forEach(function (key) { let item = t[key]; if (isObj$1(item)) { if (isAll) { item = formatter(item); result[key] = item; } else { if (key === host || key === domain) { item = formatter(item); result[key] = item; } } } }); return result } /* 判断所提供的配置任务是否适用于当前URL */ isMatch (taskConf) { const url = window.location.href; let isMatch = false; if (!taskConf.include && !taskConf.exclude) { isMatch = true; } else { if (taskConf.include && taskConf.include.test(url)) { isMatch = true; } if (taskConf.exclude && taskConf.exclude.test(url)) { isMatch = false; } } return isMatch } /** * 获取任务配置,只能获取到当前域名下的任务配置信息 * @param taskName {string} -可选 指定具体任务,默认返回所有类型的任务配置 */ getTaskConfig () { const t = this; if (!t._hasFormatTCC_) { t.formatTCC(); t._hasFormatTCC_ = true; } const domain = t.getDomain(); const taskConf = t.conf[window.location.host] || t.conf[domain]; if (taskConf && t.isMatch(taskConf)) { return taskConf } return {} } /** * 执行当前页面下的相应任务 * @param taskName {object|string} -必选,可直接传入任务配置对象,也可用是任务名称的字符串信息,自己去查找是否有任务需要执行 * @param data {object} -可选,传给回调函数的数据 */ doTask (taskName, data) { const t = this; let isDo = false; if (!taskName) return isDo const taskConf = isObj$1(taskName) ? taskName : t.getTaskConfig(); if (!isObj$1(taskConf) || !taskConf[taskName]) return isDo const task = taskConf[taskName]; if (task) { isDo = t.doTaskFunc(taskName, taskConf, data); } return isDo } }; class Debug { constructor (config = {}) { this.config = { msg: '[Debug Msg]', /* 显示调用栈信息 */ trace: false, /* 是否把调用栈信息和要打印的信息放在一组折叠起来,直接输出的话再大量较多信息的时候会显得非常凌乱,所以默认true */ traceGroup: true, printTime: false, /* 统一设置字体颜色,背景颜色,其它样式等 */ color: '#000000', backgroundColor: 'transparent', style: '', ...config, /* 为不同的调试方法设置不同的字体颜色,背景颜色,其它样式等 */ colorMap: { info: '#2274A5', log: '#95B46A', warn: '#F5A623', error: '#D33F49', ...config.colorMap || {} }, backgroundColorMap: { info: '', log: '', warn: '', error: '', ...config.backgroundColorMap || {} }, styleMap: { info: '', log: '', warn: '', error: '', ...config.styleMap || {} } }; const debugMethodList = ['log', 'error', 'info', 'warn']; debugMethodList.forEach((name) => { this[name] = this.createDebugMethod(name); }); } create (msg) { return new Debug(msg) } createDebugMethod (name) { name = name || 'info'; const { msg, color, colorMap, backgroundColor, backgroundColorMap, style, styleMap, printTime, trace, traceGroup } = this.config; const textColor = colorMap[name] || color; const bgColor = backgroundColorMap[name] || backgroundColor; const customStyle = styleMap[name] || style; return function () { if (!window._debugMode_) { return false } const arg = Array.from(arguments); const arg0 = arg[0]; arg.unshift(`color: ${textColor}; background-color: ${bgColor}; ${customStyle}`); let timeStr = ''; if (printTime) { const curTime = new Date(); const H = curTime.getHours(); const M = curTime.getMinutes(); const S = curTime.getSeconds(); timeStr = `[${H}:${M}:${S}] `; } arg.unshift(`%c ${timeStr}${msg} `); if (trace) { if (traceGroup) { const arg1Str = typeof arg0 === 'string' ? arg0 : Object.prototype.toString.call(arg0); console.groupCollapsed(`%c ${timeStr}${msg} ${arg1Str}`, `color: ${textColor}; background-color: ${bgColor}; ${customStyle}`); window.console[name].apply(console, arg); console.trace(); console.groupEnd(); } else { window.console[name].apply(console, arg); console.trace(); } } else { window.console[name].apply(window.console, arg); } } } isDebugMode () { return Boolean(window._debugMode_) } } // function demo () { // window._debugMode_ = true // window.debug = new Debug({ // msg: '[Debug Message]', // colorMap: { // info: '#FFFFFF', // log: '#FFFFFF' // }, // backgroundColorMap: { // info: '#2274A5', // log: '#95B46A' // }, // style: 'font-size: 22px; font-weight: bold; padding: 2px 4px; border-radius: 2px;', // trace: true, // traceGroup: true, // printTime: true // }) // window.debug.log('debug mode is on', window.debug) // window.debug.info('debug mode is on', window.debug) // window.debug.warn('debug mode is on', window.debug) // window.debug.error('debug mode is on', window.debug) // } // demo() var Debug$1 = new Debug(); var debug = Debug$1.create({ msg: '[H5player Msg]', trace: false, traceGroup: true, printTime: false }); const $q = function (str) { return document.querySelector(str) }; /** * 任务配置中心 Task Control Center * 用于配置所有无法进行通用处理的任务,如不同网站的全屏方式不一样,必须调用网站本身的全屏逻辑,才能确保字幕、弹幕等正常工作 * */ const taskConf = { /** * 配置示例 * 父级键名对应的是一级域名, * 子级键名对应的相关功能名称,键值对应的该功能要触发的点击选择器或者要调用的相关函数 * 所有子级的键值都支持使用选择器触发或函数调用 * 配置了子级的则使用子级配置逻辑进行操作,否则使用默认逻辑 * 注意:include,exclude这两个子级键名除外,这两个是用来进行url范围匹配的 * */ 'demo.demo': { // disable: true, // 在该域名下禁止插件的所有功能 init: function (h5Player, taskConf) {}, fullScreen: '.fullscreen-btn', exitFullScreen: '.exit-fullscreen-btn', webFullScreen: function () {}, exitWebFullScreen: '.exit-fullscreen-btn', autoPlay: '.player-start-btn', // pause: ['.player-pause', '.player-pause02'], //多种情况对应不同的选择器时,可使用数组,插件会对选择器进行遍历,知道找到可用的为止 pause: '.player-pause', play: '.player-play', afterPlay: function (h5Player, taskConf) {}, afterPause: function (h5Player, taskConf) {}, switchPlayStatus: '.player-play', playbackRate: function () {}, // playbackRate: true, // 当给某个功能设置true时,表示使用网站自身的能力控制视频,而忽略插件的能力 currentTime: function () {}, addCurrentTime: '.add-currenttime', subtractCurrentTime: '.subtract-currenttime', // 自定义快捷键的执行方式,如果是组合键,必须是 ctrl-->shift-->alt 这样的顺序,没有可以忽略,键名必须全小写 shortcuts: { /* 注册要执行自定义回调操作的快捷键 */ register: [ 'ctrl+shift+alt+c', 'ctrl+shift+c', 'ctrl+alt+c', 'ctrl+c', 'c' ], /* 自定义快捷键的回调操作 */ callback: function (h5Player, taskConf, data) { const { event, player } = data; console.log(event, player); } }, /* 阻止网站自身的调速行为,增强突破调速限制的能力 */ blockSetPlaybackRate: true, /* 阻止网站自身的播放进度控制逻辑,增强突破进度调控限制的能力 */ blockSetCurrentTime: true, /* 阻止网站自身的音量控制逻辑,排除网站自身的调音干扰 */ blockSetVolume: true, /* 当前域名下需包含的路径信息,默认整个域名下所有路径可用 必须是正则 */ include: /^.*/, /* 当前域名下需排除的路径信息,默认不排除任何路径 必须是正则 */ exclude: /\t/ }, 'youtube.com': { init: function (h5Player, taskConf) { if (h5Player.hasBindSkipAdEvents) { return } const startTime = new Date().getTime(); let skipCount = 0; const skipHandler = (element) => { const endTime = new Date().getTime(); const time = endTime - startTime; /* 过早触发会导致广告无法跳过 */ if (time < 3000) { return false } /* 页面处于不可见状态时候也不触发 */ if (document.hidden) { return false } element.click(); skipCount++; debug.log('youtube.com ad skip count', skipCount); }; ready('.ytp-ad-skip-button', function (element) { skipHandler(element); }); ready('.ytp-ad-skip-button-modern', function (element) { skipHandler(element); }); setInterval(function () { const adSkipBtn = document.querySelector('.ytp-ad-skip-button'); const adSkipBtnModern = document.querySelector('.ytp-ad-skip-button-modern'); adSkipBtn && skipHandler(adSkipBtn); adSkipBtnModern && skipHandler(adSkipBtnModern); }, 1000); h5Player.hasBindSkipAdEvents = true; }, webFullScreen: 'button.ytp-size-button', fullScreen: 'button.ytp-fullscreen-button', next: '.ytp-next-button', afterPlay: function (h5Player, taskConf) { /* 解决字幕显示停滞问题 */ setTimeout(() => { h5Player.setCurrentTimeUp(0.01, true); }, 0); /* 解决快捷键暂停、播放后一直有loading图标滞留的问题 */ const player = h5Player.player(); const playerwWrap = player.closest('.html5-video-player'); if (!playerwWrap) { return } playerwWrap.classList.add('ytp-autohide', 'playing-mode'); clearTimeout(playerwWrap.autohideTimer); playerwWrap.autohideTimer = setTimeout(() => { playerwWrap.classList.add('ytp-autohide', 'playing-mode'); }, 1000); if (!playerwWrap.hasBindCustomEvents) { const mousemoveHander = (event) => { playerwWrap.classList.remove('ytp-autohide', 'ytp-hide-info-bar'); clearTimeout(playerwWrap.mousemoveTimer); playerwWrap.mousemoveTimer = setTimeout(() => { if (!player.paused) { playerwWrap.classList.add('ytp-autohide', 'ytp-hide-info-bar'); } }, 1000 * 2); }; const clickHander = (event) => { h5Player.switchPlayStatus(); mousemoveHander(); }; player.addEventListener('mousemove', mousemoveHander); player.addEventListener('click', clickHander); playerwWrap.hasBindCustomEvents = true; } const spinner = playerwWrap.querySelector('.ytp-spinner'); if (spinner) { const hiddenSpinner = () => { spinner && (spinner.style.visibility = 'hidden'); }; const visibleSpinner = () => { spinner && (spinner.style.visibility = 'visible'); }; /* 点击播放时立即隐藏spinner */ hiddenSpinner(); clearTimeout(playerwWrap.spinnerTimer); playerwWrap.spinnerTimer = setTimeout(() => { /* 1秒后将spinner设置为none,并且恢复Spinner的可见状态,以便其它逻辑仍能正确控制spinner的显隐状态 */ spinner.style.display = 'none'; visibleSpinner(); }, 1000); } }, afterPause: function (h5Player, taskConf) { const player = h5Player.player(); const playerwWrap = player.closest('.html5-video-player'); if (!playerwWrap) return playerwWrap.classList.remove('ytp-autohide', 'playing-mode'); playerwWrap.classList.add('paused-mode'); clearTimeout(playerwWrap.autohideTimer); }, shortcuts: { register: [ 'escape' ], callback: function (h5Player, taskConf, data) { const { event } = data; if (event.keyCode === 27) { /* 取消播放下一个推荐的视频 */ if (document.querySelector('.ytp-upnext').style.display !== 'none') { document.querySelector('.ytp-upnext-cancel-button').click(); } } } } }, 'netflix.com': { // 停止在netflix下使用插件的所有功能 // disable: true, fullScreen: 'button.button-nfplayerFullscreen', addCurrentTime: 'button.button-nfplayerFastForward', subtractCurrentTime: 'button.button-nfplayerBackTen', /** * 使用netflix自身的调速,因为目前插件没法解决调速导致的服务中断问题 * https://github.com/xxxily/h5player/issues/234 * https://github.com/xxxily/h5player/issues/317 * https://github.com/xxxily/h5player/issues/381 * https://github.com/xxxily/h5player/issues/179 * https://github.com/xxxily/h5player/issues/147 */ playbackRate: true, shortcuts: { /** * TODO * netflix 一些用户习惯使用F键进行全屏,所以此处屏蔽掉f键的下一帧功能 * 后续开放自定义配置能力后,让用户自行决定是否屏蔽 */ register: [ 'f' ], callback: function (h5Player, taskConf, data) { return true } } }, 'bilibili.com': { fullScreen: function () { const fullScreen = $q('.bpx-player-ctrl-full') || $q('.squirtle-video-fullscreen') || $q('.bilibili-player-video-btn-fullscreen'); if (fullScreen) { fullScreen.click(); return true } }, webFullScreen: function () { const oldWebFullscreen = $q('.bilibili-player-video-web-fullscreen'); const webFullscreenEnter = $q('.bpx-player-ctrl-web-enter') || $q('.squirtle-pagefullscreen-inactive'); const webFullscreenLeave = $q('.bpx-player-ctrl-web-leave') || $q('.squirtle-pagefullscreen-active'); if (oldWebFullscreen || (webFullscreenEnter && webFullscreenLeave)) { const webFullscreen = oldWebFullscreen || (getComputedStyle(webFullscreenLeave).display === 'none' ? webFullscreenEnter : webFullscreenLeave); webFullscreen.click(); /* 取消弹幕框聚焦,干扰了快捷键的操作 */ setTimeout(function () { const danmaku = $q('.bpx-player-dm-input') || $q('.bilibili-player-video-danmaku-input'); danmaku && danmaku.blur(); }, 1000 * 0.1); return true } }, autoPlay: ['.bpx-player-ctrl-play', '.squirtle-video-start', '.bilibili-player-video-btn-start'], switchPlayStatus: ['.bpx-player-ctrl-play', '.squirtle-video-start', '.bilibili-player-video-btn-start'], next: ['.bpx-player-ctrl-next', '.squirtle-video-next', '.bilibili-player-video-btn-next', '.bpx-player-ctrl-btn[aria-label="下一个"]'], init: function (h5Player, taskConf) {}, shortcuts: { register: [ 'escape' ], callback: function (h5Player, taskConf, data) { const { event } = data; if (event.keyCode === 27) { /* 退出网页全屏 */ const oldWebFullscreen = $q('.bilibili-player-video-web-fullscreen'); if (oldWebFullscreen && oldWebFullscreen.classList.contains('closed')) { oldWebFullscreen.click(); } else { const webFullscreenLeave = $q('.bpx-player-ctrl-web-leave') || $q('.squirtle-pagefullscreen-active'); if (getComputedStyle(webFullscreenLeave).display !== 'none') { webFullscreenLeave.click(); } } } } } }, 't.bilibili.com': { fullScreen: 'button[name="fullscreen-button"]' }, 'live.bilibili.com': { init: function () { if (!JSON._stringifySource_) { JSON._stringifySource_ = JSON.stringify; JSON.stringify = function (arg1) { try { return JSON._stringifySource_.apply(this, arguments) } catch (e) { console.error('JSON.stringify 解释出错:', e, arg1); } }; } }, fullScreen: '.bilibili-live-player-video-controller-fullscreen-btn button', webFullScreen: '.bilibili-live-player-video-controller-web-fullscreen-btn button', switchPlayStatus: '.bilibili-live-player-video-controller-start-btn button' }, 'acfun.cn': { fullScreen: '[data-bind-key="screenTip"]', webFullScreen: '[data-bind-key="webTip"]', switchPlayStatus: function (h5player) { /* 无法抢得控制权,只好延迟判断要不要干预 */ const player = h5player.player(); const status = player.paused; setTimeout(function () { if (status === player.paused) { if (player.paused) { player.play(); } else { player.pause(); } } }, 200); } }, 'ixigua.com': { fullScreen: ['xg-fullscreen.xgplayer-fullscreen', '.xgplayer-control-item__entry[aria-label="全屏"]', '.xgplayer-control-item__entry[aria-label="退出全屏"]'], webFullScreen: ['xg-cssfullscreen.xgplayer-cssfullscreen', '.xgplayer-control-item__entry[aria-label="剧场模式"]', '.xgplayer-control-item__entry[aria-label="退出剧场模式"]'] }, 'tv.sohu.com': { fullScreen: 'button[data-title="网页全屏"]', webFullScreen: 'button[data-title="全屏"]' }, 'iqiyi.com': { fullScreen: '.iqp-btn-fullscreen', webFullScreen: '.iqp-btn-webscreen', next: '.iqp-btn-next', init: function (h5Player, taskConf) { // 隐藏水印 hideDom('.iqp-logo-box'); // 移除暂停广告 window.GM_addStyle(` div[templatetype="common_pause"]{ display:none } .iqp-logo-box{ display:none !important } `); } }, 'youku.com': { fullScreen: '.control-fullscreen-icon', next: '.control-next-video', init: function (h5Player, taskConf) { // 隐藏水印 hideDom('.youku-layer-logo'); } }, 'ted.com': { fullScreen: 'button.Fullscreen' }, 'qq.com': { pause: '.container_inner .txp-shadow-mod', play: '.container_inner .txp-shadow-mod', shortcuts: { register: ['c', 'x', 'z', '1', '2', '3', '4'], callback: function (h5Player, taskConf, data) { const { event } = data; const key = event.key.toLowerCase(); const keyName = 'customShortcuts_' + key; if (!h5Player[keyName]) { /* 第一次按下快捷键使用默认逻辑进行调速 */ h5Player[keyName] = { time: Date.now(), playbackRate: h5Player.playbackRate }; return false } else { /* 第一次操作后的200ms内的操作都是由默认逻辑进行调速 */ if (Date.now() - h5Player[keyName].time < 200) { return false } /* 判断是否需进行降级处理,利用sessionStorage进行调速 */ if (h5Player[keyName] === h5Player.playbackRate || h5Player[keyName] === true) { if (window.sessionStorage.playbackRate && /(c|x|z|1|2|3|4)/.test(key)) { const curSpeed = Number(window.sessionStorage.playbackRate); const perSpeed = curSpeed - 0.1 >= 0 ? curSpeed - 0.1 : 0.1; const nextSpeed = curSpeed + 0.1 <= 4 ? curSpeed + 0.1 : 4; let targetSpeed = curSpeed; switch (key) { case 'z' : targetSpeed = 1; break case 'c' : targetSpeed = nextSpeed; break case 'x' : targetSpeed = perSpeed; break default : targetSpeed = Number(key); break } window.sessionStorage.playbackRate = targetSpeed; h5Player.setCurrentTimeUp(0.01, true); h5Player.setPlaybackRate(targetSpeed, true); return true } /* 标识默认调速方案失效,需启用sessionStorage调速方案 */ h5Player[keyName] = true; } else { /* 标识默认调速方案生效 */ h5Player[keyName] = false; } } } }, fullScreen: 'txpdiv[data-report="window-fullscreen"]', webFullScreen: 'txpdiv[data-report="browser-fullscreen"]', next: 'txpdiv[data-report="play-next"]', init: function (h5Player, taskConf) { // 隐藏水印 hideDom('.txp-watermark'); hideDom('.txp-watermark-action'); }, include: /(v.qq|sports.qq)/ }, 'pan.baidu.com': { fullScreen: function (h5Player, taskConf) { h5Player.player().parentNode.querySelector('.vjs-fullscreen-control').click(); } }, // 'pornhub.com': { // fullScreen: 'div[class*="icon-fullscreen"]', // webFullScreen: 'div[class*="icon-size-large"]' // }, 'facebook.com': { fullScreen: function (h5Player, taskConf) { const actionBtn = h5Player.player().parentNode.querySelectorAll('button'); if (actionBtn && actionBtn.length > 3) { /* 模拟点击倒数第二个按钮 */ actionBtn[actionBtn.length - 2].click(); return true } }, webFullScreen: function (h5Player, taskConf) { const actionBtn = h5Player.player().parentNode.querySelectorAll('button'); if (actionBtn && actionBtn.length > 3) { /* 模拟点击倒数第二个按钮 */ actionBtn[actionBtn.length - 2].click(); return true } }, shortcuts: { /* 在视频模式下按esc键,自动返回上一层界面 */ register: [ 'escape' ], /* 自定义快捷键的回调操作 */ callback: function (h5Player, taskConf, data) { eachParentNode(h5Player.player(), function (parentNode) { if (parentNode.getAttribute('data-fullscreen-container') === 'true') { const goBackBtn = parentNode.parentNode.querySelector('div>a>i>u'); if (goBackBtn) { goBackBtn.parentNode.parentNode.click(); } return true } }); } } }, 'douyu.com': { fullScreen: function (h5Player, taskConf) { const player = h5Player.player(); const container = player._fullScreen_.getContainer(); if (player._isFullScreen_) { container.querySelector('div[title="退出窗口全屏"]').click(); } else { container.querySelector('div[title="窗口全屏"]').click(); } player._isFullScreen_ = !player._isFullScreen_; return true }, webFullScreen: function (h5Player, taskConf) { const player = h5Player.player(); const container = player._fullScreen_.getContainer(); if (player._isWebFullScreen_) { container.querySelector('div[title="退出网页全屏"]').click(); } else { container.querySelector('div[title="网页全屏"]').click(); } player._isWebFullScreen_ = !player._isWebFullScreen_; return true } }, 'open.163.com': { init: function (h5Player, taskConf) { const player = h5Player.player(); /** * 不设置CORS标识,这样才能跨域截图 * https://developer.mozilla.org/zh-CN/docs/Web/HTML/CORS_enabled_image * https://developer.mozilla.org/zh-CN/docs/Web/HTML/CORS_settings_attributes */ player.setAttribute('crossOrigin', 'anonymous'); } }, 'agefans.tv': { init: function (h5Player, taskConf) { h5Player.player().setAttribute('crossOrigin', 'anonymous'); } }, 'chaoxing.com': { fullScreen: '.vjs-fullscreen-control' }, 'yixi.tv': { init: function (h5Player, taskConf) { h5Player.player().setAttribute('crossOrigin', 'anonymous'); } }, 'douyin.com': { fullScreen: '.xgplayer-fullscreen', webFullScreen: '.xgplayer-page-full-screen', next: ['.xgplayer-playswitch-next'], init: function (h5Player, taskConf) { h5Player.player().setAttribute('crossOrigin', 'anonymous'); const player = h5Player.player(); const wrapEl = player.closest('div[data-e2e="feed-item"]'); const setVideoTitle = () => { if (wrapEl && wrapEl.querySelector('.video-info-detail')) { const videoInfo = wrapEl.querySelector('.video-info-detail'); const accountNameEL = videoInfo.querySelector('.account-name'); /* 移除accountName前面的@符号 */ const accountName = accountNameEL.innerText.replace(/^@*/, ''); const titleEl = videoInfo.querySelector('.title'); const titleText = titleEl.innerText.trim(); const title = `${titleText} - ${accountName}`.replace(/[\\/:*?"<>|]/g, '-'); wrapEl.setAttribute('data-title', title); player.setAttribute('data-title', title); document.title = title; wrapEl.removeEventListener('mouseover', setVideoTitle); } }; wrapEl && wrapEl.addEventListener('mouseover', setVideoTitle); setTimeout(setVideoTitle, 1200); } }, 'live.douyin.com': { fullScreen: '.xgplayer-fullscreen', webFullScreen: '.xgplayer-page-full-screen', next: ['.xgplayer-playswitch-next'], init: function (h5Player, taskConf) { h5Player.player().setAttribute('crossOrigin', 'anonymous'); } }, 'zhihu.com': { fullScreen: ['button[aria-label="全屏"]', 'button[aria-label="退出全屏"]'], play: function (h5Player, taskConf, data) { const player = h5Player.player(); if (player && player.parentNode && player.parentNode.parentNode) { const maskWrap = player.parentNode.parentNode.querySelector('div~div:nth-child(3)'); if (maskWrap) { const mask = maskWrap.querySelector('div'); if (mask && mask.innerText === '') { mask.click(); } } } }, init: function (h5Player, taskConf) { h5Player.player().setAttribute('crossOrigin', 'anonymous'); } }, 'weibo.com': { fullScreen: ['button.wbpv-fullscreen-control'], // webFullScreen: ['div[title="关闭弹层"]', 'div.wbpv-open-layer-button'] webFullScreen: ['div.wbpv-open-layer-button'] }, 'twitter.com': { init: function (h5Player, taskConf) { const player = h5Player.player(); const wrapEl = player.closest('article[data-testid="tweet"]'); const setVideoTitle = () => { if (wrapEl && !wrapEl.getAttribute('data-title') && wrapEl.querySelector('div[data-testid="tweetText"]')) { const titleEl = wrapEl.querySelector('div[data-testid="tweetText"]'); const titleText = titleEl.innerText.trim(); const title = `${titleText}`.replace(/[\\/:*?"<>|]/g, '-'); wrapEl.setAttribute('data-title', title); player.setAttribute('data-title', title); wrapEl.removeEventListener('mouseover', setVideoTitle); } }; wrapEl && wrapEl.addEventListener('mouseover', setVideoTitle); setTimeout(setVideoTitle, 600); } } }; function h5PlayerTccInit (h5Player) { return new TCC$1(taskConf, function (taskName, taskConf, data) { try { const task = taskConf[taskName]; const wrapDom = h5Player.getPlayerWrapDom(); if (!task) { return } if (taskName === 'shortcuts') { if (isObj$1(task) && task.callback instanceof Function) { return task.callback(h5Player, taskConf, data) } } else if (task instanceof Function) { try { return task(h5Player, taskConf, data) } catch (e) { debug.error('任务配置中心的自定义函数执行失败:', taskName, taskConf, data, e); return false } } else if (typeof task === 'boolean') { return task } else { const selectorList = Array.isArray(task) ? task : [task]; for (let i = 0; i < selectorList.length; i++) { const selector = selectorList[i]; /* 触发选择器上的点击事件 */ if (wrapDom && wrapDom.querySelector(selector)) { // 在video的父元素里查找,是为了尽可能兼容多实例下的逻辑 wrapDom.querySelector(selector).click(); return true } else if (document.querySelector(selector)) { document.querySelector(selector).click(); return true } } } } catch (e) { debug.error('任务配置中心的自定义任务执行失败:', taskName, taskConf, data, e); return false } }) } function mergeTaskConf (config) { return mergeObj(taskConf, config) } /* ua伪装配置 */ const fakeConfig = { // 'tv.cctv.com': userAgentMap.iPhone.chrome, // 'v.qq.com': userAgentMap.iPad.chrome, 'open.163.com': userAgentMap.iPhone.chrome, 'm.open.163.com': userAgentMap.iPhone.chrome, /* 百度盘的非会员会使用自身的专用播放器,导致没法使用h5player,所以需要通过伪装ua来解决该问题 */ 'pan.baidu.com': userAgentMap.iPhone.safari }; function setFakeUA (ua) { const host = window.location.host; ua = ua || fakeConfig[host]; /** * 动态判断是否需要进行ua伪装 * 下面方案暂时不可用 * 由于部分网站跳转至移动端后域名不一致,形成跨域问题 * 导致无法同步伪装配置而不断死循环跳转 * eg. open.163.com * */ // let customUA = window.localStorage.getItem('_h5_player_user_agent_') // debug.log(customUA, window.location.href, window.navigator.userAgent, document.referrer) // if (customUA) { // fakeUA(customUA) // alert(customUA) // } else { // alert('ua false') // } ua && fakeUA(ua); } /** * 元素全屏API,同时兼容网页全屏 */ hackAttachShadow(); class FullScreen { constructor (dom, pageMode) { this.dom = dom; this.shadowRoot = null; this.fullStatus = false; // 默认全屏模式,如果传入pageMode则表示进行的是页面全屏操作 this.pageMode = pageMode || false; const fullPageStyle = ` ._webfullscreen_box_size_ { width: 100% !important; height: 100% !important; } ._webfullscreen_ { display: block !important; position: fixed !important; width: 100% !important; height: 100% !important; top: 0 !important; left: 0 !important; background: #000 !important; z-index: 999999 !important; } ._webfullscreen_zindex_ { z-index: 999999 !important; } `; /* 将样式插入到全局页面中 */ if (!window._hasInitFullPageStyle_ && window.GM_addStyle) { window.GM_addStyle(fullPageStyle); window._hasInitFullPageStyle_ = true; } /* 将样式插入到shadowRoot中 */ const shadowRoot = isInShadow(dom, true); if (shadowRoot) { this.shadowRoot = shadowRoot; loadCSSText(fullPageStyle, 'fullPageStyle', shadowRoot); } const t = this; window.addEventListener('keyup', (event) => { const key = event.key.toLowerCase(); if (key === 'escape') { if (t.isFull()) { t.exit(); } else if (t.isFullScreen()) { t.exitFullScreen(); } } }, true); this.getContainer(); } eachParentNode (dom, fn) { let parent = dom.parentNode; while (parent && parent.classList) { const isEnd = fn(parent, dom); parent = parent.parentNode; if (isEnd) { break } } } getContainer () { const t = this; if (t._container_) return t._container_ const d = t.dom; const domBox = d.getBoundingClientRect(); let container = d; t.eachParentNode(d, function (parentNode) { const noParentNode = !parentNode || !parentNode.getBoundingClientRect; if (noParentNode || parentNode.getAttribute('data-fullscreen-container')) { container = parentNode; return true } const parentBox = parentNode.getBoundingClientRect(); const isInsideTheBox = parentBox.width <= domBox.width && parentBox.height <= domBox.height; if (isInsideTheBox) { container = parentNode; } else { return true } }); container.setAttribute('data-fullscreen-container', 'true'); t._container_ = container; return container } isFull () { return this.dom.classList.contains('_webfullscreen_') || this.fullStatus } isFullScreen () { const d = document; return !!(d.fullscreen || d.webkitIsFullScreen || d.mozFullScreen || d.fullscreenElement || d.webkitFullscreenElement || d.mozFullScreenElement) } enterFullScreen () { const c = this.getContainer(); const enterFn = c.requestFullscreen || c.webkitRequestFullScreen || c.mozRequestFullScreen || c.msRequestFullScreen; enterFn && enterFn.call(c); } enter () { const t = this; if (t.isFull()) return const container = t.getContainer(); let needSetIndex = false; if (t.dom === container) { needSetIndex = true; } function addFullscreenStyleToParentNode (node) { t.eachParentNode(node, function (parentNode) { parentNode.classList.add('_webfullscreen_'); if (container === parentNode || needSetIndex) { needSetIndex = true; parentNode.classList.add('_webfullscreen_zindex_'); } }); } addFullscreenStyleToParentNode(t.dom); /* 判断dom自身是否需要加上webfullscreen样式 */ if (t.dom.parentNode) { const domBox = t.dom.getBoundingClientRect(); const domParentBox = t.dom.parentNode.getBoundingClientRect(); if (domParentBox.width - domBox.width >= 5) { t.dom.classList.add('_webfullscreen_'); } if (t.shadowRoot && t.shadowRoot._shadowHost) { const shadowHost = t.shadowRoot._shadowHost; const shadowHostBox = shadowHost.getBoundingClientRect(); if (shadowHostBox.width <= domBox.width) { shadowHost.classList.add('_webfullscreen_'); addFullscreenStyleToParentNode(shadowHost); } } } const fullScreenMode = !t.pageMode; if (fullScreenMode) { t.enterFullScreen(); } this.fullStatus = true; } exitFullScreen () { const d = document; const exitFn = d.exitFullscreen || d.webkitExitFullscreen || d.mozCancelFullScreen || d.msExitFullscreen; exitFn && exitFn.call(d); } exit () { const t = this; function removeFullscreenStyleToParentNode (node) { t.eachParentNode(node, function (parentNode) { parentNode.classList.remove('_webfullscreen_'); parentNode.classList.remove('_webfullscreen_zindex_'); }); } removeFullscreenStyleToParentNode(t.dom); t.dom.classList.remove('_webfullscreen_'); if (t.shadowRoot && t.shadowRoot._shadowHost) { const shadowHost = t.shadowRoot._shadowHost; shadowHost.classList.remove('_webfullscreen_'); removeFullscreenStyleToParentNode(shadowHost); } const fullScreenMode = !t.pageMode; if (fullScreenMode || t.isFullScreen()) { t.exitFullScreen(); } this.fullStatus = false; } toggle () { this.isFull() ? this.exit() : this.enter(); } } /*! * @name videoCapturer.js * @version 0.0.1 * @author Blaze * @date 2019/9/21 12:03 * @github https://github.com/xxxily */ async function setClipboard (blob) { if (navigator.clipboard) { navigator.clipboard.write([ // eslint-disable-next-line no-undef new ClipboardItem({ [blob.type]: blob }) ]).then(() => { console.info('[setClipboard] clipboard suc', blob.type); }).catch((e) => { console.error('[setClipboard] clipboard err', blob.type, e); }); } else { console.error('当前网站不支持将数据写入到剪贴板里,见:\n https://developer.mozilla.org/en-US/docs/Web/API/Clipboard'); } } var videoCapturer = { /** * 进行截图操作 * @param video {dom} -必选 video dom 标签 * @returns {boolean} */ capture (video, download, title) { if (!video) return false const t = this; const currentTime = `${Math.floor(video.currentTime / 60)}'${(video.currentTime % 60).toFixed(3)}''`; const captureTitle = title || `${document.title}_${currentTime}`; /* 截图核心逻辑 */ video.setAttribute('crossorigin', 'anonymous'); const canvas = document.createElement('canvas'); canvas.width = video.videoWidth; canvas.height = video.videoHeight; const context = canvas.getContext('2d'); context.drawImage(video, 0, 0, canvas.width, canvas.height); if (download) { t.download(canvas, captureTitle, video); } else { t.previe(canvas, captureTitle); } return canvas }, /** * 预览截取到的画面内容 * @param canvas */ previe (canvas, title) { canvas.style = 'max-width:100%'; const previewPage = window.open('', '_blank'); previewPage.document.title = `capture previe - ${title || 'Untitled'}`; previewPage.document.body.style.textAlign = 'center'; previewPage.document.body.style.background = '#000'; previewPage.document.body.appendChild(canvas); }, /** * canvas 下载截取到的内容 * @param canvas */ download (canvas, title, video) { title = title || 'videoCapturer_' + Date.now(); try { /** * 尝试复制到剪贴板 * 注意部分浏览器不支持将'image/jpeg'类型的数据写入到剪贴板,image/jpg可以,但会导致toBlob的结果为png的数据, * 所以这里新起了'image/png'来尝试复制到剪贴板,而不能将setClipboard(blob)放到下面的try里 * 另外由于下面的自动下载截图会导致页面失焦,也会造成复制到剪贴板失败,所以这里先复制到剪贴板,再进行下载 */ canvas.toBlob(function (blob) { setClipboard(blob); }, 'image/png', 0.99); } catch (e) { console.error('无法将截图复制到剪贴板。', e); } try { canvas.toBlob(function (blob) { const el = document.createElement('a'); el.download = `${title}.jpg`; el.href = URL.createObjectURL(blob); el.click(); }, 'image/jpeg', 0.99); } catch (e) { videoCapturer.previe(canvas, title); console.error('视频源受CORS标识限制,无法直接下载截图,见:\n https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS'); console.error(video, e); } } }; /** * 鼠标事件观测对象 * 用于实现鼠标事件的穿透响应,有别于pointer-events:none * pointer-events:none是设置当前层允许穿透 * 而MouseObserver是:即使不知道target上面存在多少层遮挡,一样可以响应鼠标事件 */ class MouseObserver { constructor (observeOpt) { // eslint-disable-next-line no-undef this.observer = new IntersectionObserver((infoList) => { infoList.forEach((info) => { info.target.IntersectionObserverEntry = info; }); }, observeOpt || {}); this.observeList = []; } _observe (target) { let hasObserve = false; for (let i = 0; i < this.observeList.length; i++) { const el = this.observeList[i]; if (target === el) { hasObserve = true; break } } if (!hasObserve) { this.observer.observe(target); this.observeList.push(target); } } _unobserve (target) { this.observer.unobserve(target); const newObserveList = []; this.observeList.forEach((el) => { if (el !== target) { newObserveList.push(el); } }); this.observeList = newObserveList; } /** * 增加事件绑定 * @param target {element} -必选 要绑定事件的dom对象 * @param type {string} -必选 要绑定的事件,只支持鼠标事件 * @param listener {function} -必选 符合触发条件时的响应函数 */ on (target, type, listener, options) { const t = this; t._observe(target); if (!target.MouseObserverEvent) { target.MouseObserverEvent = {}; } target.MouseObserverEvent[type] = true; if (!t._mouseObserver_) { t._mouseObserver_ = {}; } if (!t._mouseObserver_[type]) { t._mouseObserver_[type] = []; window.addEventListener(type, (event) => { t.observeList.forEach((target) => { const isVisibility = target.IntersectionObserverEntry && target.IntersectionObserverEntry.intersectionRatio > 0; const isReg = target.MouseObserverEvent[event.type] === true; if (isVisibility && isReg) { /* 判断是否符合触发侦听器事件条件 */ const bound = target.getBoundingClientRect(); const offsetX = event.x - bound.x; const offsetY = event.y - bound.y; const isNeedTap = offsetX <= bound.width && offsetX >= 0 && offsetY <= bound.height && offsetY >= 0; if (isNeedTap) { /* 执行监听回调 */ const listenerList = t._mouseObserver_[type]; listenerList.forEach((listener) => { if (listener instanceof Function) { listener.call(t, event, { x: offsetX, y: offsetY }, target); } }); } } }); }, options); } /* 将监听回调加入到事件队列 */ if (listener instanceof Function) { t._mouseObserver_[type].push(listener); } } /** * 解除事件绑定 * @param target {element} -必选 要解除事件的dom对象 * @param type {string} -必选 要解除的事件,只支持鼠标事件 * @param listener {function} -必选 绑定事件时的响应函数 * @returns {boolean} */ off (target, type, listener) { const t = this; if (!target || !type || !listener || !t._mouseObserver_ || !t._mouseObserver_[type] || !target.MouseObserverEvent || !target.MouseObserverEvent[type]) return false const newListenerList = []; const listenerList = t._mouseObserver_[type]; let isMatch = false; listenerList.forEach((listenerItem) => { if (listenerItem === listener) { isMatch = true; } else { newListenerList.push(listenerItem); } }); if (isMatch) { t._mouseObserver_[type] = newListenerList; /* 侦听器已被完全移除 */ if (newListenerList.length === 0) { delete target.MouseObserverEvent[type]; } /* 当MouseObserverEvent为空对象时移除观测对象 */ if (JSON.stringify(target.MouseObserverEvent[type]) === '{}') { t._unobserve(target); } } } } /** * 简单的i18n库 */ class I18n { constructor (config) { this._languages = {}; this._locale = this.getClientLang(); this._defaultLanguage = ''; this.init(config); } init (config) { if (!config) return false const t = this; t._locale = config.locale || t._locale; /* 指定当前要是使用的语言环境,默认无需指定,会自动读取 */ t._languages = config.languages || t._languages; t._defaultLanguage = config.defaultLanguage || t._defaultLanguage; } use () {} t (path) { const t = this; let result = t.getValByPath(t._languages[t._locale] || {}, path); /* 版本回退 */ if (!result && t._locale !== t._defaultLanguage) { result = t.getValByPath(t._languages[t._defaultLanguage] || {}, path); } return result || '' } /* 当前语言值 */ language () { return this._locale } languages () { return this._languages } changeLanguage (locale) { if (this._languages[locale]) { this._locale = locale; return locale } else { return false } } /** * 根据文本路径获取对象里面的值 * @param obj {Object} -必选 要操作的对象 * @param path {String} -必选 路径信息 * @returns {*} */ getValByPath (obj, path) { path = path || ''; const pathArr = path.split('.'); let result = obj; /* 递归提取结果值 */ for (let i = 0; i < pathArr.length; i++) { if (!result) break result = result[pathArr[i]]; } return result } /* 获取客户端当前的语言环境 */ getClientLang () { return navigator.languages ? navigator.languages[0] : navigator.language } } var zhCN = { website: '🏠脚本官网', about: '关于', issues: '问题反馈', setting: '设置', hotkeys: '快捷键', hotkeysDocs: '快捷键文档', enable: '启用', disable: '禁用', toggleStates: '启用/禁用', disableHotkeysTemporarily: '临时禁用快捷键', toggleHotkeysTemporarily: '临时启用/禁用快捷键', enableHotkeys: '启用快捷键', disableHotkeys: '禁用快捷键', donate: '👍请作者喝杯咖啡', aboutDonate: '作者收了多少打赏?', aboutAuthor: '关于作者', recommend: '❤️ 免费ChatGPT-4 ❤️', enableScript: '启用脚本', disableScript: '禁用脚本', disableCurrentInstanceGUI: '关闭当前图形用户界面', disableGUITemporarily: '临时禁用图形用户界面', enableGUI: '启用图形用户界面', disableGUI: '禁用图形用户界面', graphicalInterface: '图形界面', alwaysShowGraphicalInterface: '始终显示图形界面', openCrossOriginFramePage: '单独打开跨域的页面', disableInitAutoPlay: '禁止在此网站自动播放视频', enableInitAutoPlay: '允许在此网站自动播放视频', restoreConfiguration: '还原全局的默认配置', blockSetPlaybackRate: '禁用默认速度调节逻辑', blockSetCurrentTime: '禁用默认播放进度控制逻辑', blockSetVolume: '禁用默认音量控制逻辑', unblockSetPlaybackRate: '允许默认速度调节逻辑', unblockSetCurrentTime: '允许默认播放进度控制逻辑', unblockSetVolume: '允许默认音量控制逻辑', allowAcousticGain: '开启音量增益能力', notAllowAcousticGain: '禁用音量增益能力', allowCrossOriginControl: '开启跨域控制能力', notAllowCrossOriginControl: '禁用跨域控制能力', allowExperimentFeatures: '开启实验性功能', notAllowExperimentFeatures: '禁用实验性功能', experimentFeaturesWarning: '实验性功能容易造成一些不确定的问题,请谨慎开启', useMediaDownloadTips: '使用下载功能,需开启实验性功能,\n实验性功能容易造成一些不确定的问题,确定要开启吗?', allowExternalCustomConfiguration: '开启外部自定义能力', notAllowExternalCustomConfiguration: '关闭外部自定义能力', changeLog: '更新日志', currentVersion: '当前版本', checkVersion: '检查是否有新版本?', configFail: '配置失败', globalSetting: '全局设置', openCustomConfigurationEditor: '打开自定义配置编辑器', localSetting: '仅用于此网站', openDebugMode: '开启调试模式', closeDebugMode: '关闭调试模式', unfoldMenu: '展开菜单', foldMenu: '折叠菜单', addGroupChat: '💬添加群聊', speed: '倍速', capture: '截图', download: '下载', mediaDownload: { downloading: '文件正在下载中,确定重复执行此操作?', hasDownload: '文件已经下载,确定重复执行此操作?', confirmTitle: '请输入文件名', notSupport: '当前媒体文件无法下载,下载功能待优化完善', notEndOfStream: '媒体数据还没完全就绪,确定要执行下载操作?', cancelAutoDownload: '是否取消自动下载?', autoDownload: '媒体数据完全就绪后,是否自动下载?', notFoundMediaSource: '未找到对应的媒体流数据,数据可能被清理或者媒体元素已经被移除,建议刷新页面后重试' }, menu: '菜单', more: '更多', moreActions: '更多操作', videoFilter: '画面滤镜', resetFilterAndTransform: '图像复位', brightnessUp: '增加亮度', brightnessDown: '减少亮度', contrastUp: '增加对比度', contrastDown: '减少对比度', saturationUp: '增加饱和度', saturationDown: '减少饱和度', hueUp: '增加色相', hueDown: '减少色相', blurUp: '增加模糊度', blurDown: '减少模糊度', rotateAndMirror: '旋转镜像', rotate90: '画面旋转 90 度', horizontalMirror: '画面水平镜像翻转', verticalMirror: '画面垂直镜像翻转', videoTransform: '画面位移', translateRight: '画面向右移动', translateLeft: '画面向左移动', translateUp: '画面向上移动', translateDown: '画面向下移动', languageSettings: '语言设置', default: '默认', autoChoose: '自动选择', comingSoon: '更多功能正在完善中,敬请期待', ffmpegScript: '音视频合并/转换脚本', autoGotoBufferedTime: '自动跟随跳转到缓冲区时间', disableAutoGotoBufferedTime: '禁用自动跟随跳转到缓冲区时间', tipsMsg: { playspeed: '播放速度:', forward: '前进:', backward: '后退:', seconds: '秒', volume: '音量:', nextframe: '定位:下一帧', previousframe: '定位:上一帧', stopframe: '定格帧画面:', play: '播放', pause: '暂停', arpl: '允许自动恢复播放进度', drpl: '禁止自动恢复播放进度', brightness: '图像亮度:', contrast: '图像对比度:', saturation: '图像饱和度:', hue: '图像色相:', blur: '图像模糊度:', imgattrreset: '图像属性:复位', imgrotate: '画面旋转:', onplugin: '启用h5Player插件', offplugin: '禁用h5Player插件', globalmode: '全局模式:', playbackrestored: '为你恢复上次播放进度', playbackrestoreoff: '恢复播放进度功能已禁用,按 SHIFT+R 可开启该功能', horizontal: '水平位移:', vertical: '垂直位移:', horizontalMirror: '水平镜像', verticalMirror: '垂直镜像', videozoom: '视频缩放率:' } }; var enUS = { website: '🏠Script Homepage', about: 'About', issues: 'Issues', setting: 'Setting', hotkeys: 'Hotkeys', hotkeysDocs: 'Hotkeys Docs', enable: 'Enable', disable: 'Disable', toggleStates: 'Enable/Disable', disableHotkeysTemporarily: 'Disable hotkeys temporarily', toggleHotkeysTemporarily: 'Toggle hotkeys temporarily', enableHotkeys: 'Enable hotkeys', disableHotkeys: 'Disable hotkeys', donate: '👍Donate', aboutDonate: 'How much the author has received?', aboutAuthor: 'About the author', enableScript: 'Enable script', disableScript: 'Disable script', disableCurrentInstanceGUI: 'Close the current graphical user interface', disableGUITemporarily: 'Temporarily disable the graphical interface', enableGUI: 'Enable Graphical User Interface', disableGUI: 'Disable Graphical User Interface', graphicalInterface: 'Graphical Interface', alwaysShowGraphicalInterface: 'Always show graphical interface', openCrossOriginFramePage: 'Open cross-domain pages alone', disableInitAutoPlay: 'Prohibit autoplay of videos on this site', enableInitAutoPlay: 'Allow autoplay videos on this site', restoreConfiguration: 'Restore the global default configuration', blockSetPlaybackRate: 'Disable default speed regulation logic', blockSetCurrentTime: 'Disable default playback progress control logic', blockSetVolume: 'Disable default volume control logic', unblockSetPlaybackRate: 'Allow default speed adjustment logic', unblockSetCurrentTime: 'Allow default playback progress control logic', unblockSetVolume: 'Allow default volume control logic', allowAcousticGain: 'Turn on volume boost', notAllowAcousticGain: 'Disable volume boost ability', allowCrossOriginControl: 'Enable cross-domain control capability', notAllowCrossOriginControl: 'Disable cross-domain control capabilities', allowExperimentFeatures: 'Turn on experimental features', notAllowExperimentFeatures: 'Disable experimental features', experimentFeaturesWarning: 'Experimental features are likely to cause some uncertain problems, please turn on with caution', useMediaDownloadTips: 'To use the download capability, you need to enable experimental features.\nExperimental features can easily cause some uncertain problems. Are you sure you want to enable it?', allowExternalCustomConfiguration: 'Enable external customization capabilities', notAllowExternalCustomConfiguration: 'Turn off external customization capabilities', changeLog: 'Change log', currentVersion: 'Current version', checkVersion: 'Check for new version ?', configFail: 'Configuration failed', globalSetting: 'Global Settings', openCustomConfigurationEditor: 'Open custom configuration editor', localSetting: 'For this site only', openDebugMode: 'Enable debug mode', closeDebugMode: 'Turn off debug mode', unfoldMenu: 'Expand menu', foldMenu: 'Collapse menu', addGroupChat: '💬Add chat group', speed: 'Speed', capture: 'Capture', download: 'Download', mediaDownload: { downloading: 'The file is being downloaded. Are you sure you want to execute this operation again?', hasDownload: 'The file has been downloaded. Are you sure you want to execute this operation again?', confirmTitle: 'Please enter the file name', notSupport: 'The current media file cannot be downloaded. The download function needs to be optimized and improved', notEndOfStream: 'The media data is not fully ready, are you sure you want to download it?', cancelAutoDownload: 'Cancel automatic download?', autoDownload: 'The media data is not fully ready, do you want to automatically download it when it is ready?', notFoundMediaSource: 'The corresponding media stream data was not found, the data may have been cleared or the media element has been removed, it is recommended to refresh the page and try again' }, menu: 'Menu', more: 'More', moreActions: 'More actions', videoFilter: 'Video filter', resetFilterAndTransform: 'Reset filter and transform', brightnessUp: 'Increase brightness', brightnessDown: 'Decrease brightness', contrastUp: 'Increase contrast', contrastDown: 'Decrease contrast', saturationUp: 'Increase saturation', saturationDown: 'Decrease saturation', hueUp: 'Increase hue', hueDown: 'Decrease hue', blurUp: 'Increase blur', blurDown: 'Decrease blur', rotateAndMirror: 'Rotate and mirror', rotate90: 'Rotate 90 degrees', horizontalMirror: 'Horizontal mirror flip', verticalMirror: 'Vertical mirror flip', videoTransform: 'Video displacement', translateRight: 'Move the screen to the right', translateLeft: 'Move the screen to the left', translateUp: 'Move the screen up', translateDown: 'Move the screen down', languageSettings: 'Language settings', default: 'Default', autoChoose: 'Auto choose', comingSoon: 'More features are being improved, stay tuned', ffmpegScript: 'Audio and video merge/convert script', autoGotoBufferedTime: 'Automatically jump to the buffered time', disableAutoGotoBufferedTime: 'Disable automatic jump to the buffered time', tipsMsg: { playspeed: 'Speed: ', forward: 'Forward: ', backward: 'Backward: ', seconds: 'sec', volume: 'Volume: ', nextframe: 'Next frame', previousframe: 'Previous frame', stopframe: 'Stopframe: ', play: 'Play', pause: 'Pause', arpl: 'Allow auto resume playback progress', drpl: 'Disable auto resume playback progress', brightness: 'Brightness: ', contrast: 'Contrast: ', saturation: 'Saturation: ', hue: 'HUE: ', blur: 'Blur: ', imgattrreset: 'Attributes: reset', imgrotate: 'Picture rotation: ', onplugin: 'ON h5Player plugin', offplugin: 'OFF h5Player plugin', globalmode: 'Global mode: ', playbackrestored: 'Restored the last playback progress for you', playbackrestoreoff: 'The function of restoring the playback progress is disabled. Press SHIFT+R to turn on the function', horizontal: 'Horizontal displacement: ', vertical: 'Vertical displacement: ', horizontalMirror: 'Horizontal mirror', verticalMirror: 'vertical mirror', videozoom: 'Video zoom: ' }, demo: 'demo-test' }; var ru = { website: '🏠официальный сайт скрипта', about: 'около', issues: 'обратная связь', setting: 'установка', hotkeys: 'горячие клавиши', hotkeysDocs: 'документы горячих клавиш', enable: 'включить', disable: 'отключить', toggleStates: 'включить/отключить', disableHotkeysTemporarily: 'временно отключить горячие клавиши', toggleHotkeysTemporarily: 'временно включить/отключить горячие клавиши', enableHotkeys: 'включить горячие клавиши', disableHotkeys: 'отключить горячие клавиши', donate: '👍пожертвовать', aboutDonate: 'Сколько автор получил?', aboutAuthor: 'о авторе', enableScript: 'включить скрипт', disableScript: 'отключить скрипт', disableCurrentInstanceGUI: 'отключить текущий графический интерфейс пользователя', disableGUITemporarily: 'Временно отключить графический интерфейс пользователя', enableGUI: 'Включить графический интерфейс пользователя', disableGUI: 'Отключить графический интерфейс пользователя', graphicalInterface: 'Графический интерфейс', alwaysShowGraphicalInterface: 'Всегда показывать графический интерфейс', openCrossOriginFramePage: 'Открывать только междоменные страницы', disableInitAutoPlay: 'Запретить автовоспроизведение видео на этом сайте', enableInitAutoPlay: 'Разрешить автоматическое воспроизведение видео на этом сайте', restoreConfiguration: 'Восстановить глобальную конфигурацию по умолчанию', blockSetPlaybackRate: 'Отключить логику регулирования скорости по умолчанию', blockSetCurrentTime: 'Отключить логику управления ходом воспроизведения по умолчанию', blockSetVolume: 'Отключить логику управления громкостью по умолчанию', unblockSetPlaybackRate: 'Разрешить логику регулировки скорости по умолчанию', unblockSetCurrentTime: 'Разрешить логику управления ходом воспроизведения по умолчанию', unblockSetVolume: 'Разрешить логику управления громкостью по умолчанию', allowAcousticGain: 'Включите усиление громкости', notAllowAcousticGain: 'Отключить возможность увеличения громкости', allowCrossOriginControl: 'Включить возможность междоменного контроля', notAllowCrossOriginControl: 'Отключить возможности междоменного контроля', allowExperimentFeatures: 'Включить экспериментальные функции', notAllowExperimentFeatures: 'Отключить экспериментальные функции', experimentFeaturesWarning: 'Экспериментальные функции могут вызвать определенные проблемы, включайте их с осторожностью.', useMediaDownloadTips: 'Чтобы использовать функцию загрузки, вам необходимо включить экспериментальную функцию.\nЭкспериментальные функции могут легко вызвать некоторые неопределенные проблемы. Вы уверены, что хотите включить ее?', allowExternalCustomConfiguration: 'Включить возможности внешней настройки', notAllowExternalCustomConfiguration: 'Отключить возможности внешней настройки', changeLog: 'Журнал изменений', currentVersion: 'Текущая версия', checkVersion: 'Проверить наличие новой версии ?', configFail: 'Ошибка конфигурации', globalSetting: 'Глобальные настройки', openCustomConfigurationEditor: 'Открыть редактор пользовательской конфигурации', localSetting: 'только для этого сайта', openDebugMode: 'Включить режим отладки', closeDebugMode: 'отключить режим отладки', unfoldMenu: 'развернуть меню', foldMenu: 'свернуть меню', addGroupChat: '💬Добавить группу чата', speed: 'Скорость', capture: 'Захват', download: 'Скачать', mediaDownload: { downloading: 'Идет скачивание файла. Вы уверены, что хотите повторить эту операцию?', hasDownload: 'Файл скачан. Вы уверены, что хотите повторить эту операцию?', confirmTitle: 'Пожалуйста, введите имя файла', notSupport: 'Текущий медиафайл невозможно загрузить, а функцию загрузки необходимо оптимизировать и улучшить.', notEndOfStream: 'Медиаданные еще не полностью готовы. Вы уверены, что хотите их скачать?', cancelAutoDownload: 'Отменить автоматическую загрузку?', autoDownload: 'Будут ли медиаданные загружаться автоматически после их полной готовности?', notFoundMediaSource: 'Соответствующие данные медиапотока не найдены. Возможно, данные были очищены или медиа-элементы удалены. Рекомендуется обновить страницу и повторить попытку.' }, menu: 'Меню', more: 'Больше', moreActions: 'Дополнительные действия', videoFilter: 'Видеофильтр', resetFilterAndTransform: 'Сбросить фильтр и трансформацию', brightnessUp: 'Увеличить яркость', brightnessDown: 'Уменьшить яркость', contrastUp: 'Увеличить контраст', contrastDown: 'Уменьшить контраст', saturationUp: 'Увеличить насыщенность', saturationDown: 'Уменьшить насыщенность', hueUp: 'Увеличить оттенок', hueDown: 'Уменьшить оттенок', blurUp: 'Увеличить размытие', blurDown: 'Уменьшить размытие', rotateAndMirror: 'Повернуть и отразить', rotate90: 'Повернуть изображение на 90 градусов', horizontalMirror: 'Горизонтальное отражение изображения', verticalMirror: 'Вертикальное отражение изображения', videoTransform: 'Видео трансформация', translateRight: 'Сдвинуть экран вправо', translateLeft: 'Сдвинуть экран влево', translateUp: 'Сдвинуть экран вверх', translateDown: 'Сдвинуть экран вниз', languageSettings: 'Настройки языка', default: 'По умолчанию', autoChoose: 'Автоматический выбор', comingSoon: 'Больше функций находится в процессе улучшения, следите за обновлениями', ffmpegScript: 'Скрипт слияния/преобразования аудио и видео', autoGotoBufferedTime: 'Автоматически перейти к времени буфера', disableAutoGotoBufferedTime: 'Отключить автоматический переход к времени буфера', tipsMsg: { playspeed: 'Скорость: ', forward: 'Вперёд: ', backward: 'Назад: ', seconds: ' сек', volume: 'Громкость: ', nextframe: 'Следующий кадр', previousframe: 'Предыдущий кадр', stopframe: 'Стоп-кадр: ', play: 'Запуск', pause: 'Пауза', arpl: 'Разрешить автоматическое возобновление прогресса воспроизведения', drpl: 'Запретить автоматическое возобновление прогресса воспроизведения', brightness: 'Яркость: ', contrast: 'Контраст: ', saturation: 'Насыщенность: ', hue: 'Оттенок: ', blur: 'Размытие: ', imgattrreset: 'Атрибуты: сброс', imgrotate: 'Поворот изображения: ', onplugin: 'ВКЛ: плагин воспроизведения', offplugin: 'ВЫКЛ: плагин воспроизведения', globalmode: 'Глобальный режим:', playbackrestored: 'Восстановлен последний прогресс воспроизведения', playbackrestoreoff: 'Функция восстановления прогресса воспроизведения отключена. Нажмите SHIFT + R, чтобы включить функцию', horizontal: 'Горизонтальное смещение: ', vertical: 'Вертикальное смещение: ', horizontalMirror: 'Горизонтальное зеркало', verticalMirror: 'вертикальное зеркало', videozoom: 'Увеличить видео: ' } }; var zhTW = { website: '🏠腳本官網', about: '關於', issues: '反饋', setting: '設置', hotkeys: '快捷鍵', hotkeysDocs: '快捷鍵文檔', enable: '啟用', disable: '禁用', toggleStates: '啟用/禁用', disableHotkeysTemporarily: '臨時禁用快捷鍵', toggleHotkeysTemporarily: '臨時啟用/禁用快捷鍵', enableHotkeys: '啟用快捷鍵', disableHotkeys: '禁用快捷鍵', donate: '👍讚賞', aboutDonate: '作者收了多少打賞?', aboutAuthor: '關於作者', enableScript: '啟用腳本', disableScript: '禁用腳本', disableCurrentInstanceGUI: '關閉當前圖形用戶界面', disableGUITemporarily: '臨時禁用圖形用戶界面', enableGUI: '啟用圖形用戶界面', disableGUI: '禁用圖形用戶界面', graphicalInterface: '圖形界面', alwaysShowGraphicalInterface: '始終顯示圖形界面', openCrossOriginFramePage: '單獨打開跨域的頁面', disableInitAutoPlay: '禁止在此網站自動播放視頻', enableInitAutoPlay: '允許在此網站自動播放視頻', restoreConfiguration: '還原全局的默認配置', blockSetPlaybackRate: '禁用默認速度調節邏輯', blockSetCurrentTime: '禁用默認播放進度控制邏輯', blockSetVolume: '禁用默認音量控制邏輯', unblockSetPlaybackRate: '允許默認速度調節邏輯', unblockSetCurrentTime: '允許默認播放進度控制邏輯', unblockSetVolume: '允許默認音量控制邏輯', allowAcousticGain: '開啟音量增益能力', notAllowAcousticGain: '禁用音量增益能力', allowCrossOriginControl: '開啟跨域控制能力', notAllowCrossOriginControl: '禁用跨域控制能力', allowExperimentFeatures: '開啟實驗性功能', notAllowExperimentFeatures: '禁用實驗性功能', experimentFeaturesWarning: '實驗性功能容易造成一些不確定的問題,請謹慎開啟', useMediaDownloadTips: '使用下載功能,需開啟實驗功能,\n實驗功能容易造成一些不確定的問題,確定要開啟嗎?', allowExternalCustomConfiguration: '開啟外部自定義能力', notAllowExternalCustomConfiguration: '關閉外部自定義能力', changeLog: '更新日誌', currentVersion: '當前版本', checkVersion: '檢查是否有新版本?', configFail: '配置失敗', globalSetting: '全局設置', openCustomConfigurationEditor: '打開自定義配置編輯器', localSetting: '僅用於此網站', openDebugMode: '開啟調試模式', closeDebugMode: '關閉調試模式', unfoldMenu: '展開菜單', foldMenu: '折疊菜單', addGroupChat: '💬新增群聊', speed: '倍速', capture: '截圖', download: '下載', mediaDownload: { downloading: '文件正在下載中,確定重複執行此操作?', hasDownload: '文件已經下載,確定重複執行此操作?', confirmTitle: '請輸入文件名', notSupport: '目前媒體檔案無法下載,下載功能要優化完善', notEndOfStream: '媒體資料還沒完全就緒,確定要執行下載操作?', cancelAutoDownload: '是否取消自動下載?', autoDownload: '媒體資料完全就緒後,是否自動下載?', notFoundMediaSource: '未找到對應的媒體流數據,數據可能被清理或媒體元素已經被移除,建議刷新頁面後重試' }, menu: '菜單', more: '更多', moreActions: '更多操作', videoFilter: '視頻濾鏡', resetFilterAndTransform: '圖像復位', brightnessUp: '增加亮度', brightnessDown: '減少亮度', contrastUp: '增加對比度', contrastDown: '減少對比度', saturationUp: '增加飽和度', saturationDown: '減少飽和度', hueUp: '增加色相', hueDown: '減少色相', blurUp: '增加模糊度', blurDown: '減少模糊度', rotateAndMirror: '旋轉和鏡像', rotate90: '畫面旋轉 90 度', horizontalMirror: '畫面水平鏡像翻轉', verticalMirror: '畫面垂直鏡像翻轉', videoTransform: '畫面位移', translateRight: '畫面向右移動', translateLeft: '畫面向左移動', translateUp: '畫面向上移動', translateDown: '畫面向下移動', languageSettings: '語言設置', default: '默認', autoChoose: '自動選擇', comingSoon: '更多功能正在完善中,敬請期待', ffmpegScript: '音視頻合併/轉換腳本', autoGotoBufferedTime: '自動跟隨跳轉到緩衝區時間', disableAutoGotoBufferedTime: '禁用自動跟隨跳轉到緩衝區時間', tipsMsg: { playspeed: '播放速度:', forward: '向前:', backward: '向後:', seconds: '秒', volume: '音量:', nextframe: '定位:下一幀', previousframe: '定位:上一幀', stopframe: '定格幀畫面:', play: '播放', pause: '暫停', arpl: '允許自動恢復播放進度', drpl: '禁止自動恢復播放進度', brightness: '圖像亮度:', contrast: '圖像對比度:', saturation: '圖像飽和度:', hue: '圖像色相:', blur: '圖像模糊度:', imgattrreset: '圖像屬性:復位', imgrotate: '畫面旋轉:', onplugin: '啟用h5Player插件', offplugin: '禁用h5Player插件', globalmode: '全局模式:', playbackrestored: '為你恢復上次播放進度', playbackrestoreoff: '恢復播放進度功能已禁用,按 SHIFT+R 可開啟該功能', horizontal: '水平位移:', vertical: '垂直位移:', horizontalMirror: '水平鏡像', verticalMirror: '垂直鏡像', videozoom: '視頻縮放率:' } }; const messages = { 'zh-CN': zhCN, zh: zhCN, 'zh-HK': zhTW, 'zh-TW': zhTW, 'en-US': enUS, en: enUS, ru }; const i18n = new I18n({ defaultLanguage: 'en', /* 指定当前要是使用的语言环境,默认无需指定,会自动读取 */ // locale: 'zh-TW', languages: messages }); const lang = configManager.get('language'); lang && i18n.changeLanguage(lang); /* 用于获取全局唯一的id */ let __globalId__ = 0; function getId () { if (window.GM_getValue && window.GM_setValue) { let gID = window.GM_getValue('_global_id_'); if (!gID) gID = 0; gID = Number(gID) + 1; window.GM_setValue('_global_id_', gID); return gID } else { /* 如果不处于油猴插件下,则该id为页面自己独享的id */ __globalId__ = Number(__globalId__) + 1; return __globalId__ } } let curTabId = null; /** * 获取当前TAB标签的Id号,可用于iframe确定自己是否处于同一TAB标签下 * @returns {Promise} */ function getTabId () { return new Promise((resolve, reject) => { if (window.GM_getTab instanceof Function) { window.GM_getTab(function (obj) { if (!obj.tabId) { obj.tabId = getId(); window.GM_saveTab(obj); } /* 每次获取都更新当前Tab的id号 */ curTabId = obj.tabId; resolve(obj.tabId); }); } else { /* 非油猴插件下,无法确定iframe是否处于同一个tab下 */ resolve(Date.now()); } }) } /* 一开始就初始化好curTabId,这样后续就不需要异步获取Tabid,部分场景下需要用到 */ getTabId(); /*! * @name monkeyMsg.js * @version 0.0.1 * @author Blaze * @date 2019/9/21 14:22 */ // import debug from './debug' /** * 将对象数据里面可存储到GM_setValue里面的值提取出来 * @param obj {objcet} -必选 打算要存储的对象数据 * @param deep {number} -可选 如果对象层级非常深,则须限定递归的层级,默认最高不能超过3级 * @returns {{}} */ function extractDatafromOb (obj, deep) { deep = deep || 1; if (deep > 3) return {} const result = {}; if (typeof obj === 'object') { for (const key in obj) { const val = obj[key]; const valType = typeof val; if (valType === 'number' || valType === 'string' || valType === 'boolean') { Object.defineProperty(result, key, { value: val, writable: true, configurable: true, enumerable: true }); } else if (valType === 'object' && Object.prototype.propertyIsEnumerable.call(obj, key)) { /* 进行递归提取 */ result[key] = extractDatafromOb(val, deep + 1); } else if (valType === 'array') { result[key] = val; } else ; } } return result } const monkeyMsg = { /** * 发送消息,除了正常发送信息外,还会补充各类必要的信息 * @param name {string} -必选 要发送给那个字段,接收时要一致才能监听的正确 * @param data {Any} -必选 要发送的数据 * @param throttleInterval -可选,因为会出现莫名奇妙的重复发送情况,为了消除重复发送带来的副作用,所以引入节流限制逻辑,即限制某个时间间隔内只能发送一次,多余的次数自动抛弃掉,默认80ms * @returns {Promise} */ send (name, data, throttleInterval = 80) { if (!window.GM_getValue || !window.GM_setValue) { return false } /* 阻止频繁发送修改事件 */ const oldMsg = window.GM_getValue(name); if (oldMsg && oldMsg.updateTime) { const interval = Math.abs(Date.now() - oldMsg.updateTime); if (interval < throttleInterval) { return false } } const msg = { /* 发送过来的数据 */ data, /* 补充标签ID,用于判断是否同处一个tab标签下 */ tabId: curTabId || 'undefined', /* 补充消息的页面来源的标题信息 */ title: document.title, /* 补充消息的页面来源信息 */ referrer: extractDatafromOb(window.location), /* 最近一次更新该数据的时间 */ updateTime: Date.now() }; if (typeof data === 'object') { msg.data = extractDatafromOb(data); } window.GM_setValue(name, msg); // debug.info(`[monkeyMsg-send][${name}]`, msg) }, set: (name, data) => monkeyMsg.send(name, data), get: (name) => window.GM_getValue && window.GM_getValue(name), on: (name, fn) => window.GM_addValueChangeListener && window.GM_addValueChangeListener(name, function (name, oldVal, newVal, remote) { // debug.info(`[monkeyMsg-on][${name}]`, oldVal, newVal, remote) /* 补充消息来源是否出自同一个Tab的判断字段 */ newVal.originTab = newVal.tabId === curTabId; fn instanceof Function && fn.apply(null, arguments); }), off: (listenerId) => window.GM_removeValueChangeListener && window.GM_removeValueChangeListener(listenerId), /** * 进行monkeyMsg的消息广播,该广播每两秒钟发送一次,其它任意页面可通接收到的广播信息来更新一些变量信息 * 主要用以解决通过setInterval或setTimeout因页面可视状态和性能策略导致的不运行或执行频率异常而不能正确更新变量状态的问题 * 见: https://developer.mozilla.org/zh-CN/docs/Web/API/Page_Visibility_API * 广播也不能100%保证不受性能策略的影响,但只要有一个网页处于前台运行,则就能正常工作 * @param handler {Function} -必选 接收到广播信息时的回调函数 * @returns */ broadcast (handler) { const broadcastName = '__monkeyMsgBroadcast__'; monkeyMsg._monkeyMsgBroadcastHandler_ = monkeyMsg._monkeyMsgBroadcastHandler_ || []; handler instanceof Function && monkeyMsg._monkeyMsgBroadcastHandler_.push(handler); if (monkeyMsg._hasMonkeyMsgBroadcast_) { return broadcastName } monkeyMsg.on(broadcastName, function () { monkeyMsg._monkeyMsgBroadcastHandler_.forEach(handler => { handler.apply(null, arguments); }); }); setInterval(function () { /* 通过限定时间间隔来防止多个页面批量发起广播信息 */ const data = monkeyMsg.get(broadcastName); if (data && Date.now() - data.updateTime < 1000 * 2) { return false } monkeyMsg.send(broadcastName, {}); }, 1000 * 2); return broadcastName } }; /*! * @name crossTabCtl.js * @description 跨Tab控制脚本逻辑 * @version 0.0.1 * @author Blaze * @date 2019/11/21 上午11:56 * @github https://github.com/xxxily */ const crossTabCtl = { /* 在进行跨Tab控制时,排除转发的快捷键,以减少对重要快捷键的干扰 */ excludeShortcuts (event) { if (!event || typeof event.keyCode === 'undefined') { return false } const excludeKeyCode = ['c', 'v', 'f', 'd']; if (event.ctrlKey || event.metaKey) { const key = event.key.toLowerCase(); if (excludeKeyCode.includes(key)) { return true } else { return false } } else { return false } }, /* 意外退出的时候leavepictureinpicture事件并不会被调用,所以只能通过轮询来更新画中画信息 */ updatePictureInPictureInfo () { setInterval(function () { if (document.pictureInPictureElement) { monkeyMsg.send('globalPictureInPictureInfo', { usePictureInPicture: true }); } }, 1000 * 1.5); /** * 通过setInterval来更新globalPictureInPictureInfo会受页面可见性和性能策略影响而得不到更新 * 见: https://developer.mozilla.org/zh-CN/docs/Web/API/Page_Visibility_API * 所以通过增加monkeyMsg广播机制来校准globalPictureInPictureInfo状态 */ monkeyMsg.broadcast(function () { // console.log('[monkeyMsg][broadcast]', ...arguments) if (document.pictureInPictureElement) { monkeyMsg.send('globalPictureInPictureInfo', { usePictureInPicture: true }); } }); }, /* 判断当前是否开启了画中画功能 */ hasOpenPictureInPicture () { const data = monkeyMsg.get('globalPictureInPictureInfo'); /* 画中画的全局信息更新时间差在3s内,才认为当前开启了画中画模式,否则有可能意外退出,而没修改usePictureInPicture的值,造成误判 */ if (data && data.data) { if (data.data.usePictureInPicture) { return Math.abs(Date.now() - data.updateTime) < 1000 * 3 } else { /** * 检测到画中画已经被关闭,但还没关闭太久的话,允许有个短暂的时间段内让用户跨TAB控制视频 * 例如:暂停视频播放 */ return Math.abs(Date.now() - data.updateTime) < 1000 * 15 } } return false }, /** * 判断是否需要发送跨Tab控制按键信息 */ isNeedSendCrossTabCtlEvent () { const t = crossTabCtl; /* 画中画开启后,判断不在同一个Tab才发送事件 */ const data = monkeyMsg.get('globalPictureInPictureInfo'); if (t.hasOpenPictureInPicture() && data.tabId !== curTabId) { return true } else { return false } }, crossTabKeydownEvent (event) { const t = crossTabCtl; /* 处于可编辑元素中不执行任何快捷键 */ const target = event.composedPath ? event.composedPath()[0] || event.target : event.target; if (isEditableTarget(target)) return if (t.isNeedSendCrossTabCtlEvent() && isRegisterKey(event) && !t.excludeShortcuts(event)) { // 阻止事件冒泡和默认事件 event.stopPropagation(); event.preventDefault(); /* 广播按键消息,进行跨Tab控制 */ // keydownEvent里已经包含了globalKeydownEvent事件 // monkeyMsg.send('globalKeydownEvent', event) return true } }, bindCrossTabEvent () { const t = crossTabCtl; if (t._hasBindEvent_) return document.removeEventListener('keydown', t.crossTabKeydownEvent); document.addEventListener('keydown', t.crossTabKeydownEvent, true); t._hasBindEvent_ = true; }, init () { const t = crossTabCtl; t.updatePictureInPictureInfo(); t.bindCrossTabEvent(); } }; /*! * @name index.js * @description hookJs JS AOP切面编程辅助库 * @version 0.0.1 * @author Blaze * @date 2020/10/22 17:40 * @github https://github.com/xxxily */ const win = typeof window === 'undefined' ? global : window; const toStr = Function.prototype.call.bind(Object.prototype.toString); /* 特殊场景,如果把Boolean也hook了,很容易导致调用溢出,所以是需要使用原生Boolean */ const toBoolean = Boolean.originMethod ? Boolean.originMethod : Boolean; const util = { toStr, isObj: obj => toStr(obj) === '[object Object]', /* 判断是否为引用类型,用于更宽泛的场景 */ isRef: obj => typeof obj === 'object', isReg: obj => toStr(obj) === '[object RegExp]', isFn: obj => obj instanceof Function, isAsyncFn: fn => toStr(fn) === '[object AsyncFunction]', isPromise: obj => toStr(obj) === '[object Promise]', firstUpperCase: str => str.replace(/^\S/, s => s.toUpperCase()), toArr: arg => Array.from(Array.isArray(arg) ? arg : [arg]), debug: { log () { let log = win.console.log; /* 如果log也被hook了,则使用未被hook前的log函数 */ if (log.originMethod) { log = log.originMethod; } if (win._debugMode_) { log.apply(win.console, arguments); } } }, /* 获取包含自身、继承、可枚举、不可枚举的键名 */ getAllKeys (obj) { const tmpArr = []; for (const key in obj) { tmpArr.push(key); } const allKeys = Array.from(new Set(tmpArr.concat(Reflect.ownKeys(obj)))); return allKeys } }; class HookJs { constructor (useProxy) { this.useProxy = useProxy || false; this.hookPropertiesKeyName = '_hookProperties' + Date.now(); } hookJsPro () { return new HookJs(true) } _addHook (hookMethod, fn, type, classHook) { const hookKeyName = type + 'Hooks'; const hookMethodProperties = hookMethod[this.hookPropertiesKeyName]; if (!hookMethodProperties[hookKeyName]) { hookMethodProperties[hookKeyName] = []; } /* 注册(储存)要被调用的hook函数,同时防止重复注册 */ let hasSameHook = false; for (let i = 0; i < hookMethodProperties[hookKeyName].length; i++) { if (fn === hookMethodProperties[hookKeyName][i]) { hasSameHook = true; break } } if (!hasSameHook) { fn.classHook = classHook || false; hookMethodProperties[hookKeyName].push(fn); } } _runHooks (parentObj, methodName, originMethod, hookMethod, target, ctx, args, classHook, hookPropertiesKeyName) { const hookMethodProperties = hookMethod[hookPropertiesKeyName]; const beforeHooks = hookMethodProperties.beforeHooks || []; const afterHooks = hookMethodProperties.afterHooks || []; const errorHooks = hookMethodProperties.errorHooks || []; const hangUpHooks = hookMethodProperties.hangUpHooks || []; const replaceHooks = hookMethodProperties.replaceHooks || []; const execInfo = { result: null, error: null, args: args, type: '' }; function runHooks (hooks, type) { let hookResult = null; execInfo.type = type || ''; if (Array.isArray(hooks)) { hooks.forEach(fn => { if (util.isFn(fn) && classHook === fn.classHook) { hookResult = fn(args, parentObj, methodName, originMethod, execInfo, ctx); } }); } return hookResult } const runTarget = (function () { if (classHook) { return function () { // eslint-disable-next-line new-cap return new target(...args) } } else { return function () { return target.apply(ctx, args) } } })(); const beforeHooksResult = runHooks(beforeHooks, 'before'); /* 支持终止后续调用的指令 */ if (beforeHooksResult && beforeHooksResult === 'STOP-INVOKE') { return beforeHooksResult } if (hangUpHooks.length || replaceHooks.length) { /** * 当存在hangUpHooks或replaceHooks的时候是不会触发原来函数的 * 本质上来说hangUpHooks和replaceHooks是一样的,只是外部的定义描述不一致和分类不一致而已 */ runHooks(hangUpHooks, 'hangUp'); runHooks(replaceHooks, 'replace'); } else { if (errorHooks.length) { try { execInfo.result = runTarget(); } catch (err) { execInfo.error = err; const errorHooksResult = runHooks(errorHooks, 'error'); /* 支持执行错误后不抛出异常的指令 */ if (errorHooksResult && errorHooksResult === 'SKIP-ERROR') ; else { throw err } } } else { execInfo.result = runTarget(); } } /** * 执行afterHooks,如果返回的是Promise,理论上应该进行进一步的细分处理 * 但添加细分处理逻辑后发现性能下降得比较厉害,且容易出现各种异常,所以决定不在hook里处理Promise情况 * 下面是原Promise处理逻辑,添加后会导致以下网站卡死或无法访问: * wenku.baidu.com * https://pubs.rsc.org/en/content/articlelanding/2021/sc/d1sc01881g#!divAbstract * https://www.elsevier.com/connect/coronavirus-information-center */ // if (execInfo.result && execInfo.result.then && util.isPromise(execInfo.result)) { // execInfo.result.then(function (data) { // execInfo.result = data // runHooks(afterHooks, 'after') // return Promise.resolve.apply(ctx, arguments) // }).catch(function (err) { // execInfo.error = err // runHooks(errorHooks, 'error') // return Promise.reject.apply(ctx, arguments) // }) // } runHooks(afterHooks, 'after'); return execInfo.result } _proxyMethodcGenerator (parentObj, methodName, originMethod, classHook, context, proxyHandler) { const t = this; const useProxy = t.useProxy; let hookMethod = null; /* 存在缓存则使用缓存的hookMethod */ if (t.isHook(originMethod)) { hookMethod = originMethod; } else if (originMethod[t.hookPropertiesKeyName] && t.isHook(originMethod[t.hookPropertiesKeyName].hookMethod)) { hookMethod = originMethod[t.hookPropertiesKeyName].hookMethod; } if (hookMethod) { if (!hookMethod[t.hookPropertiesKeyName].isHook) { /* 重新标注被hook状态 */ hookMethod[t.hookPropertiesKeyName].isHook = true; util.debug.log(`[hook method] ${util.toStr(parentObj)} ${methodName}`); } return hookMethod } /* 使用Proxy模式进行hook可以获得更多特性,但性能也会稍差一些 */ if (useProxy && Proxy) { /* 注意:使用Proxy代理,hookMethod和originMethod将共用同一对象 */ const handler = { ...proxyHandler }; /* 下面的写法确定了proxyHandler是无法覆盖construct和apply操作的 */ if (classHook) { handler.construct = function (target, args, newTarget) { context = context || this; return t._runHooks(parentObj, methodName, originMethod, hookMethod, target, context, args, true, t.hookPropertiesKeyName) }; } else { handler.apply = function (target, ctx, args) { ctx = context || ctx; return t._runHooks(parentObj, methodName, originMethod, hookMethod, target, ctx, args, false, t.hookPropertiesKeyName) }; } hookMethod = new Proxy(originMethod, handler); } else { hookMethod = function () { /** * 注意此处不能通过 context = context || this * 然后通过把context当ctx传递过去 * 这将导致ctx引用错误 */ const ctx = context || this; return t._runHooks(parentObj, methodName, originMethod, hookMethod, originMethod, ctx, arguments, classHook, t.hookPropertiesKeyName) }; /* 确保子对象和原型链跟originMethod保持一致 */ const keys = Reflect.ownKeys(originMethod); keys.forEach(keyName => { try { Object.defineProperty(hookMethod, keyName, { get: function () { return originMethod[keyName] }, set: function (val) { originMethod[keyName] = val; } }); } catch (err) { // 设置defineProperty的时候出现异常,可能导致hookMethod部分功能缺失,也可能不受影响 util.debug.log(`[proxyMethodcGenerator] hookMethod defineProperty abnormal. hookMethod:${methodName}, definePropertyName:${keyName}`, err); } }); hookMethod.prototype = originMethod.prototype; } const hookMethodProperties = hookMethod[t.hookPropertiesKeyName] = {}; hookMethodProperties.originMethod = originMethod; hookMethodProperties.hookMethod = hookMethod; hookMethodProperties.isHook = true; hookMethodProperties.classHook = classHook; util.debug.log(`[hook method] ${util.toStr(parentObj)} ${methodName}`); return hookMethod } _getObjKeysByRule (obj, rule) { let excludeRule = null; let result = rule; if (util.isObj(rule) && rule.include) { excludeRule = rule.exclude; rule = rule.include; result = rule; } /** * for in、Object.keys与Reflect.ownKeys的区别见: * https://es6.ruanyifeng.com/#docs/object#%E5%B1%9E%E6%80%A7%E7%9A%84%E9%81%8D%E5%8E%86 */ if (rule === '*') { result = Object.keys(obj); } else if (rule === '**') { result = Reflect.ownKeys(obj); } else if (rule === '***') { result = util.getAllKeys(obj); } else if (util.isReg(rule)) { result = util.getAllKeys(obj).filter(keyName => rule.test(keyName)); } /* 如果存在排除规则,则需要进行排除 */ if (excludeRule) { result = Array.isArray(result) ? result : [result]; if (util.isReg(excludeRule)) { result = result.filter(keyName => !excludeRule.test(keyName)); } else if (Array.isArray(excludeRule)) { result = result.filter(keyName => !excludeRule.includes(keyName)); } else { result = result.filter(keyName => excludeRule !== keyName); } } return util.toArr(result) } /** * 判断某个函数是否已经被hook * @param fn {Function} -必选 要判断的函数 * @returns {boolean} */ isHook (fn) { if (!fn || !fn[this.hookPropertiesKeyName]) { return false } const hookMethodProperties = fn[this.hookPropertiesKeyName]; return util.isFn(hookMethodProperties.originMethod) && fn !== hookMethodProperties.originMethod } /** * 判断对象下的某个值是否具备hook的条件 * 注意:具备hook条件和能否直接修改值是两回事, * 在进行hook的时候还要检查descriptor.writable是否为false * 如果为false则要修改成true才能hook成功 * @param parentObj * @param keyName * @returns {boolean} */ isAllowHook (parentObj, keyName) { /* 有些对象会设置getter,让读取值的时候就抛错,所以需要try catch 判断能否正常读取属性 */ try { if (!parentObj[keyName]) return false } catch (e) { return false } const descriptor = Object.getOwnPropertyDescriptor(parentObj, keyName); return !(descriptor && descriptor.configurable === false) } /** * hook 核心函数 * @param parentObj {Object} -必选 被hook函数依赖的父对象 * @param hookMethods {Object|Array|RegExp|string} -必选 被hook函数的函数名或函数名的匹配规则 * @param fn {Function} -必选 hook之后的回调方法 * @param type {String} -可选 默认before,指定运行hook函数回调的时机,可选字符串:before、after、replace、error、hangUp * @param classHook {Boolean} -可选 默认false,指定是否为针对new(class)操作的hook * @param context {Object} -可选 指定运行被hook函数时的上下文对象 * @param proxyHandler {Object} -可选 仅当用Proxy进行hook时有效,默认使用的是Proxy的apply handler进行hook,如果你有特殊需求也可以配置自己的handler以实现更复杂的功能 * 附注:不使用Proxy进行hook,可以获得更高性能,但也意味着通用性更差些,对于要hook HTMLElement.prototype、EventTarget.prototype这些对象里面的非实例的函数往往会失败而导致被hook函数执行出错 * @returns {boolean} */ hook (parentObj, hookMethods, fn, type, classHook, context, proxyHandler) { /* 支持对象形式的传参 */ const opts = arguments[0]; if (util.isObj(opts) && opts.parentObj && opts.hookMethods) { parentObj = opts.parentObj; hookMethods = opts.hookMethods; fn = opts.fn; type = opts.type; classHook = opts.classHook; context = opts.context; proxyHandler = opts.proxyHandler; } classHook = toBoolean(classHook); type = type || 'before'; if ((!util.isRef(parentObj) && !util.isFn(parentObj)) || !util.isFn(fn) || !hookMethods) { return false } const t = this; hookMethods = t._getObjKeysByRule(parentObj, hookMethods); hookMethods.forEach(methodName => { if (!t.isAllowHook(parentObj, methodName)) { util.debug.log(`${util.toStr(parentObj)} [${methodName}] does not support modification`); return false } const descriptor = Object.getOwnPropertyDescriptor(parentObj, methodName); if (descriptor && descriptor.writable === false) { Object.defineProperty(parentObj, methodName, { writable: true }); } const originMethod = parentObj[methodName]; let hookMethod = null; /* 非函数无法进行hook操作 */ if (!util.isFn(originMethod)) { return false } hookMethod = t._proxyMethodcGenerator(parentObj, methodName, originMethod, classHook, context, proxyHandler); const hookMethodProperties = hookMethod[t.hookPropertiesKeyName]; if (hookMethodProperties.classHook !== classHook) { util.debug.log(`${util.toStr(parentObj)} [${methodName}] Cannot support functions hook and classes hook at the same time `); return false } /* 使用hookMethod接管需要被hook的方法 */ if (parentObj[methodName] !== hookMethod) { parentObj[methodName] = hookMethod; } t._addHook(hookMethod, fn, type, classHook); }); } /* 专门针对new操作的hook,本质上是hook函数的别名,可以少传classHook这个参数,并且明确语义 */ hookClass (parentObj, hookMethods, fn, type, context, proxyHandler) { return this.hook(parentObj, hookMethods, fn, type, true, context, proxyHandler) } /** * 取消对某个函数的hook * @param parentObj {Object} -必选 要取消被hook函数依赖的父对象 * @param hookMethods {Object|Array|RegExp|string} -必选 要取消被hook函数的函数名或函数名的匹配规则 * @param type {String} -可选 默认before,指定要取消的hook类型,可选字符串:before、after、replace、error、hangUp,如果不指定该选项则取消所有类型下的所有回调 * @param fn {Function} -必选 取消指定的hook回调函数,如果不指定该选项则取消对应type类型下的所有回调 * @returns {boolean} */ unHook (parentObj, hookMethods, type, fn) { if (!util.isRef(parentObj) || !hookMethods) { return false } const t = this; hookMethods = t._getObjKeysByRule(parentObj, hookMethods); hookMethods.forEach(methodName => { if (!t.isAllowHook(parentObj, methodName)) { return false } const hookMethod = parentObj[methodName]; if (!t.isHook(hookMethod)) { return false } const hookMethodProperties = hookMethod[t.hookPropertiesKeyName]; const originMethod = hookMethodProperties.originMethod; if (type) { const hookKeyName = type + 'Hooks'; const hooks = hookMethodProperties[hookKeyName] || []; if (fn) { /* 删除指定类型下的指定hook函数 */ for (let i = 0; i < hooks.length; i++) { if (fn === hooks[i]) { hookMethodProperties[hookKeyName].splice(i, 1); util.debug.log(`[unHook ${hookKeyName} func] ${util.toStr(parentObj)} ${methodName}`, fn); break } } } else { /* 删除指定类型下的所有hook函数 */ if (Array.isArray(hookMethodProperties[hookKeyName])) { hookMethodProperties[hookKeyName] = []; util.debug.log(`[unHook all ${hookKeyName}] ${util.toStr(parentObj)} ${methodName}`); } } } else { /* 彻底还原被hook的函数 */ if (util.isFn(originMethod)) { parentObj[methodName] = originMethod; delete parentObj[methodName][t.hookPropertiesKeyName]; // Object.keys(hookMethod).forEach(keyName => { // if (/Hooks$/.test(keyName) && Array.isArray(hookMethod[keyName])) { // hookMethod[keyName] = [] // } // }) // // hookMethod.isHook = false // parentObj[methodName] = originMethod // delete parentObj[methodName].originMethod // delete parentObj[methodName].hookMethod // delete parentObj[methodName].isHook // delete parentObj[methodName].isClassHook util.debug.log(`[unHook method] ${util.toStr(parentObj)} ${methodName}`); } } }); } _hook (args, type) { const t = this; return function (obj, hookMethods, fn, classHook, context, proxyHandler) { const opts = args[0]; if (util.isObj(opts) && opts.parentObj && opts.hookMethods) { opts.type = type; } return t.hook.apply(t, args) } } /* 源函数运行前的hook */ before (obj, hookMethods, fn, classHook, context, proxyHandler) { return this.hook(obj, hookMethods, fn, 'before', classHook, context, proxyHandler) } /* 源函数运行后的hook */ after (obj, hookMethods, fn, classHook, context, proxyHandler) { return this.hook(obj, hookMethods, fn, 'after', classHook, context, proxyHandler) } /* 替换掉要hook的函数,不再运行源函数,换成运行其他逻辑 */ replace (obj, hookMethods, fn, classHook, context, proxyHandler) { return this.hook(obj, hookMethods, fn, 'replace', classHook, context, proxyHandler) } /* 源函数运行出错时的hook */ error (obj, hookMethods, fn, classHook, context, proxyHandler) { return this.hook(obj, hookMethods, fn, 'error', classHook, context, proxyHandler) } /* 底层实现逻辑与replace一样,都是替换掉要hook的函数,不再运行源函数,只不过是为了明确语义,将源函数挂起不再执行,原则上也不再执行其他逻辑,如果要执行其他逻辑请使用replace hook */ hangUp (obj, hookMethods, fn, classHook, context, proxyHandler) { return this.hook(obj, hookMethods, fn, 'hangUp', classHook, context, proxyHandler) } } const hookJs = new HookJs(true); /** * 禁止对playbackRate进行锁定 * 部分播放器会阻止修改playbackRate * 通过hackDefineProperty来反阻止playbackRate的修改 * 参考: https://greasyfork.org/zh-CN/scripts/372673 */ function hackDefineProperCore (target, key, option) { if (option && target && target instanceof Element && typeof key === 'string' && key.indexOf('on') >= 0) { option.configurable = true; } if (target instanceof HTMLVideoElement) { const unLockProperties = ['playbackRate', 'currentTime', 'volume', 'muted']; if (unLockProperties.includes(key)) { try { debug.log(`禁止对${key}进行锁定`); option.configurable = true; key = key + '_hack'; } catch (e) { debug.error(`禁止锁定${key}失败!`, e); } } } return [target, key, option] } function hackDefineProperOnError (args, parentObj, methodName, originMethod, execInfo, ctx) { debug.error(`${methodName} error:`, execInfo.error); /* 忽略执行异常 */ return 'SKIP-ERROR' } function hackDefineProperty () { hookJs.before(Object, 'defineProperty', function (args, parentObj, methodName, originMethod, execInfo, ctx) { const option = args[2]; const ele = args[0]; const key = args[1]; const afterArgs = hackDefineProperCore(ele, key, option); afterArgs.forEach((arg, i) => { args[i] = arg; }); }); hookJs.before(Object, 'defineProperties', function (args, parentObj, methodName, originMethod, execInfo, ctx) { const properties = args[1]; const ele = args[0]; if (ele && ele instanceof Element) { Object.keys(properties).forEach(key => { const option = properties[key]; const afterArgs = hackDefineProperCore(ele, key, option); args[0] = afterArgs[0]; delete properties[key]; properties[afterArgs[1]] = afterArgs[2]; }); } }); hookJs.error(Object, 'defineProperty', hackDefineProperOnError); hookJs.error(Object, 'defineProperties', hackDefineProperOnError); } /*! * @name menuCommand.js * @version 0.0.1 * @author Blaze * @date 2019/9/21 14:22 */ const monkeyMenu = { menuIds: {}, on (title, fn, accessKey) { if (title instanceof Function) { title = title(); } if (window.GM_registerMenuCommand) { const menuId = window.GM_registerMenuCommand(title, fn, accessKey); this.menuIds[menuId] = { title, fn, accessKey }; return menuId } }, off (id) { if (window.GM_unregisterMenuCommand) { delete this.menuIds[id]; /** * 批量移除已注册的按钮时,在某些性能较差的机子上会留下数字title的菜单残留 * 应该属于插件自身导致的BUG,暂时无法解决 * 所以此处暂时不进行菜单移除,tampermonkey会自动对同名菜单进行合并 */ // return window.GM_unregisterMenuCommand(id) } }, clear () { Object.keys(this.menuIds).forEach(id => { this.off(id); }); }, /** * 通过菜单配置进行批量注册,注册前会清空之前注册过的所有菜单 * @param {array|function} menuOpts 菜单配置,如果是函数则会调用该函数获取菜单配置,并且当菜单被点击后会重新创建菜单,实现菜单的动态更新 */ build (menuOpts) { this.clear(); if (Array.isArray(menuOpts)) { menuOpts.forEach(menu => { if (menu.disable === true) { return } this.on(menu.title, menu.fn, menu.accessKey); }); } else if (menuOpts instanceof Function) { const menuList = menuOpts(); if (Array.isArray(menuList)) { this._menuBuilder_ = menuOpts; menuList.forEach(menu => { if (menu.disable === true) { return } const menuFn = () => { try { menu.fn.apply(menu, arguments); } catch (e) { console.error('[monkeyMenu]', menu.title, e); } // 每次菜单点击后,重新注册菜单,这样可以确保菜单的状态是最新的 setTimeout(() => { // console.log('[monkeyMenu rebuild]', menu.title) this.build(this._menuBuilder_); }, 100); }; this.on(menu.title, menuFn, menu.accessKey); }); } else { console.error('monkeyMenu build error, no menuList return', menuOpts); } } } }; const version = '4.2.2'; function refreshPage (msg) { msg = msg || '配置已更改,马上刷新页面让配置生效?'; const status = confirm(msg); if (status) { window.location.reload(); } } /** * 全局可调用的功能,会提供给monkeyMenu调用和UI界面的相关位置进行调用 * 为了便于调用编排所以使用对象的方式进行管理 */ const globalFunctional = { openInTab, getHomePageLink: { title: i18n.t('website'), desc: i18n.t('website'), fn: () => { const homePageLinks = [ 'https://h5player.anzz.top', 'https://github.com/xxxily/h5player', 'https://greasyfork.org/scripts/381682', 'https://u.anzz.top/h5player' ]; /* 从homePageLinks中随机选取一个链接返回 */ return homePageLinks[Math.floor(Math.random() * homePageLinks.length)] } }, /* 打开官网 */ openWebsite: { title: i18n.t('website'), desc: i18n.t('website'), fn: () => { openInTab('https://u.anzz.top/h5player'); } }, openAuthorHomePage: { title: i18n.t('aboutAuthor'), desc: i18n.t('aboutAuthor'), fn: () => { // openInTab('https://github.com/xxxily') openInTab('https://u.anzz.top/xxxily'); } }, openHotkeysPage: { title: i18n.t('hotkeysDocs'), desc: i18n.t('hotkeysDocs'), fn: () => { openInTab('https://h5player.anzz.top/home/Introduction.html#%E5%BF%AB%E6%8D%B7%E9%94%AE%E5%88%97%E8%A1%A8'); } }, openProjectGithub: { title: 'GitHub', desc: 'GitHub', fn: () => { openInTab('https://github.com/xxxily/h5player'); } }, openIssuesPage: { title: i18n.t('issues'), desc: i18n.t('hotkeys'), fn: () => { openInTab('https://github.com/xxxily/h5player/issues'); } }, openDonatePage: { title: i18n.t('donate'), desc: i18n.t('donate'), fn: () => { openInTab('https://u.anzz.top/h5playerdonate'); } }, openAboutDonatePage: { title: i18n.t('aboutDonate'), desc: i18n.t('aboutDonate'), fn: () => { openInTab('https://u.anzz.top/aboutonate'); } }, openAddGroupChatPage: { title: i18n.t('addGroupChat'), desc: i18n.t('addGroupChat'), fn: () => { openInTab('https://u.anzz.top/h5playerddhatroup'); } }, openChangeLogPage: { title: i18n.t('changeLog'), desc: i18n.t('changeLog'), fn: () => { openInTab('https://h5player.anzz.top/home/changeLog.html'); } }, openCheckVersionPage: { title: i18n.t('checkVersion'), desc: i18n.t('checkVersion'), fn: () => { const confirm = window.confirm(`${i18n.t('currentVersion')}「${version}」\n${i18n.t('checkVersion')}`); if (confirm) { openInTab('https://greasyfork.org/zh-CN/scripts/381682/versions'); } } }, openRecommendPage: { title: i18n.t('recommend'), desc: i18n.t('recommend'), fn: () => { function randomZeroOrOne () { return Math.floor(Math.random() * 2) } if (randomZeroOrOne()) { openInTab('https://hello-ai.anzz.top/home/'); } else { openInTab('https://github.com/xxxily/hello-ai'); } } }, openCustomConfigurationEditor: { title: i18n.t('openCustomConfigurationEditor'), desc: i18n.t('openCustomConfigurationEditor'), fn: () => { // openInTab('https://h5player.anzz.top/tools/json-editor/index.html?mode=tree&saveHandlerName=saveH5PlayerConfig&expandAll=true&json={}') openInTab('https://u.anzz.top/h5pjsoneditor'); } }, /* 切换tampermonkey菜单的展开或折叠状态 */ toggleExpandedOrCollapsedStateOfMonkeyMenu: { title: `${configManager.get('enhance.unfoldMenu') ? i18n.t('foldMenu') : i18n.t('unfoldMenu')} 「${i18n.t('globalSetting')}」`, desc: `${configManager.get('enhance.unfoldMenu') ? i18n.t('foldMenu') : i18n.t('unfoldMenu')} 「${i18n.t('globalSetting')}」`, fn: () => { const confirm = window.confirm(configManager.get('enhance.unfoldMenu') ? i18n.t('foldMenu') : i18n.t('unfoldMenu')); if (confirm) { configManager.setGlobalStorage('enhance.unfoldMenu', !configManager.get('enhance.unfoldMenu')); window.location.reload(); } } }, /* 切换脚本的启用或禁用状态 */ toggleScriptEnableState: { title: `${configManager.get('enable') ? i18n.t('disableScript') : i18n.t('enableScript')} 「${i18n.t('localSetting')}」`, desc: `${configManager.get('enable') ? i18n.t('disableScript') : i18n.t('enableScript')} 「${i18n.t('localSetting')}」`, fn: () => { const confirm = window.confirm(configManager.get('enable') ? i18n.t('disableScript') : i18n.t('enableScript')); if (confirm) { configManager.setLocalStorage('enable', !configManager.get('enable')); window.location.reload(); } } }, /* 切换默认播放进度的控制逻辑 */ toggleSetCurrentTimeFunctional: { /* 标题使用函数是为了下次调用的时候读取到最新的状态信息 */ title: () => `${configManager.get('enhance.blockSetCurrentTime') ? i18n.t('unblockSetCurrentTime') : i18n.t('blockSetCurrentTime')} 「${i18n.t('localSetting')}」`, desc: () => `${configManager.get('enhance.blockSetCurrentTime') ? i18n.t('unblockSetCurrentTime') : i18n.t('blockSetCurrentTime')} 「${i18n.t('localSetting')}」`, fn: () => { const confirm = window.confirm(configManager.get('enhance.blockSetCurrentTime') ? i18n.t('unblockSetCurrentTime') : i18n.t('blockSetCurrentTime')); if (confirm) { configManager.setLocalStorage('enhance.blockSetCurrentTime', !configManager.get('enhance.blockSetCurrentTime')); window.location.reload(); } } }, toggleSetVolumeFunctional: { title: () => `${configManager.get('enhance.blockSetVolume') ? i18n.t('unblockSetVolume') : i18n.t('blockSetVolume')} 「${i18n.t('localSetting')}」`, desc: () => `${configManager.get('enhance.blockSetVolume') ? i18n.t('unblockSetVolume') : i18n.t('blockSetVolume')} 「${i18n.t('localSetting')}」`, fn: () => { const confirm = window.confirm(configManager.get('enhance.blockSetVolume') ? i18n.t('unblockSetVolume') : i18n.t('blockSetVolume')); if (confirm) { configManager.setLocalStorage('enhance.blockSetVolume', !configManager.get('enhance.blockSetVolume')); window.location.reload(); } } }, toggleSetPlaybackRateFunctional: { title: () => `${configManager.get('enhance.blockSetPlaybackRate') ? i18n.t('unblockSetPlaybackRate') : i18n.t('blockSetPlaybackRate')} 「${i18n.t('globalSetting')}」`, desc: () => `${configManager.get('enhance.blockSetPlaybackRate') ? i18n.t('unblockSetPlaybackRate') : i18n.t('blockSetPlaybackRate')} 「${i18n.t('globalSetting')}」`, fn: () => { const confirm = window.confirm(configManager.get('enhance.blockSetPlaybackRate') ? i18n.t('unblockSetPlaybackRate') : i18n.t('blockSetPlaybackRate')); if (confirm) { /* 倍速参数,只能全局设置 */ configManager.setGlobalStorage('enhance.blockSetPlaybackRate', !configManager.get('enhance.blockSetPlaybackRate')); window.location.reload(); } } }, toggleAcousticGainFunctional: { title: () => `${configManager.get('enhance.allowAcousticGain') ? i18n.t('notAllowAcousticGain') : i18n.t('allowAcousticGain')} 「${i18n.t('globalSetting')}」`, desc: () => `${configManager.get('enhance.allowAcousticGain') ? i18n.t('notAllowAcousticGain') : i18n.t('allowAcousticGain')} 「${i18n.t('globalSetting')}」`, fn: () => { const confirm = window.confirm(configManager.get('enhance.allowAcousticGain') ? i18n.t('notAllowAcousticGain') : i18n.t('allowAcousticGain')); if (confirm) { configManager.setGlobalStorage('enhance.allowAcousticGain', !configManager.getGlobalStorage('enhance.allowAcousticGain')); window.location.reload(); } } }, toggleCrossOriginControlFunctional: { title: () => `${configManager.get('enhance.allowCrossOriginControl') ? i18n.t('notAllowCrossOriginControl') : i18n.t('allowCrossOriginControl')} 「${i18n.t('globalSetting')}」`, desc: () => `${configManager.get('enhance.allowCrossOriginControl') ? i18n.t('notAllowCrossOriginControl') : i18n.t('allowCrossOriginControl')} 「${i18n.t('globalSetting')}」`, fn: () => { const confirm = window.confirm(configManager.get('enhance.allowCrossOriginControl') ? i18n.t('notAllowCrossOriginControl') : i18n.t('allowCrossOriginControl')); if (confirm) { configManager.setGlobalStorage('enhance.allowCrossOriginControl', !configManager.getGlobalStorage('enhance.allowCrossOriginControl')); window.location.reload(); } } }, toggleExperimentFeatures: { title: () => `${configManager.get('enhance.allowExperimentFeatures') ? i18n.t('notAllowExperimentFeatures') : i18n.t('allowExperimentFeatures')} 「${i18n.t('globalSetting')}」`, desc: () => `${configManager.get('enhance.allowExperimentFeatures') ? i18n.t('notAllowExperimentFeatures') : i18n.t('allowExperimentFeatures')} 「${i18n.t('globalSetting')}」`, fn: () => { const confirm = window.confirm(configManager.get('enhance.allowExperimentFeatures') ? i18n.t('notAllowExperimentFeatures') : i18n.t('experimentFeaturesWarning')); if (confirm) { configManager.setGlobalStorage('enhance.allowExperimentFeatures', !configManager.get('enhance.allowExperimentFeatures')); window.location.reload(); } } }, toggleExternalCustomConfiguration: { title: () => `${configManager.get('enhance.allowExternalCustomConfiguration') ? i18n.t('notAllowExternalCustomConfiguration') : i18n.t('allowExternalCustomConfiguration')} 「${i18n.t('globalSetting')}」`, desc: () => `${configManager.get('enhance.allowExternalCustomConfiguration') ? i18n.t('notAllowExternalCustomConfiguration') : i18n.t('allowExternalCustomConfiguration')} 「${i18n.t('globalSetting')}」`, fn: () => { const confirm = window.confirm(configManager.get('enhance.allowExternalCustomConfiguration') ? i18n.t('notAllowExternalCustomConfiguration') : i18n.t('allowExternalCustomConfiguration')); if (confirm) { configManager.setGlobalStorage('enhance.allowExternalCustomConfiguration', !configManager.getGlobalStorage('enhance.allowExternalCustomConfiguration')); window.location.reload(); } } }, toggleDebugMode: { title: () => `${configManager.getGlobalStorage('debug') ? i18n.t('closeDebugMode') : i18n.t('openDebugMode')} 「${i18n.t('globalSetting')}」`, desc: () => `${configManager.getGlobalStorage('debug') ? i18n.t('closeDebugMode') : i18n.t('openDebugMode')} 「${i18n.t('globalSetting')}」`, fn: () => { const confirm = window.confirm(configManager.getGlobalStorage('debug') ? i18n.t('closeDebugMode') : i18n.t('openDebugMode')); if (confirm) { configManager.setGlobalStorage('debug', !configManager.getGlobalStorage('debug')); window.location.reload(); } } }, /* 还原全局的默认配置 */ restoreGlobalConfiguration: { title: i18n.t('restoreConfiguration'), desc: i18n.t('restoreConfiguration'), fn: () => { configManager.clear(); refreshPage(); } }, openCrossOriginFramePage: { title: i18n.t('openCrossOriginFramePage'), desc: i18n.t('openCrossOriginFramePage'), fn: () => { openInTab(location.href); } }, /* 切换脚本UI界面的显示或隐藏状态,注意:只有明确为fasle才隐藏GUI,其它情况都要显示GUI,例如null、undefined等都正常显示GUI */ toggleGUIStatus: { title: () => `${configManager.getGlobalStorage('ui.enable') === false ? i18n.t('enableGUI') : i18n.t('disableGUI')} 「${i18n.t('globalSetting')}」`, desc: () => `${configManager.getGlobalStorage('ui.enable') === false ? i18n.t('enableGUI') : i18n.t('disableGUI')} 「${i18n.t('globalSetting')}」`, fn: () => { const confirm = window.confirm(`${configManager.getGlobalStorage('ui.enable') === false ? i18n.t('enableGUI') : i18n.t('disableGUI')} 「${i18n.t('globalSetting')}」`); if (confirm) { configManager.setGlobalStorage('ui.enable', !configManager.getGlobalStorage('ui.enable')); window.location.reload(); } } }, /* 切换当前网站下的脚本UI界面的显示或隐藏状态 */ toggleGUIStatusUnderCurrentSite: { title: () => `${configManager.getLocalStorage('ui.enable') === false ? i18n.t('enableGUI') : i18n.t('disableGUI')} 「${i18n.t('localSetting')}」`, desc: () => `${configManager.getLocalStorage('ui.enable') === false ? i18n.t('enableGUI') : i18n.t('disableGUI')} 「${i18n.t('localSetting')}」`, fn: () => { const confirm = window.confirm(`${configManager.getLocalStorage('ui.enable') === false ? i18n.t('enableGUI') : i18n.t('disableGUI')} 「${i18n.t('localSetting')}」`); if (confirm) { configManager.setLocalStorage('ui.enable', !configManager.getLocalStorage('ui.enable')); window.location.reload(); } } }, alwaysShowGraphicalInterface: { title: `${i18n.t('toggleStates')}${i18n.t('alwaysShowGraphicalInterface')} 「${i18n.t('globalSetting')}」`, desc: `${i18n.t('toggleStates')}${i18n.t('alwaysShowGraphicalInterface')} 「${i18n.t('globalSetting')}」`, fn: () => { const alwaysShow = configManager.getGlobalStorage('ui.alwaysShow'); const confirm = window.confirm(alwaysShow === true ? `${i18n.t('disable')}${i18n.t('alwaysShowGraphicalInterface')} 「${i18n.t('globalSetting')}」` : `${i18n.t('alwaysShowGraphicalInterface')} 「${i18n.t('globalSetting')}」`); if (confirm) { configManager.setGlobalStorage('ui.alwaysShow', !alwaysShow); window.location.reload(); } } }, toggleHotkeysStatus: { title: () => `${configManager.getGlobalStorage('enableHotkeys') === false ? i18n.t('enableHotkeys') : i18n.t('disableHotkeys')} 「${i18n.t('globalSetting')}」`, desc: () => `${configManager.getGlobalStorage('enableHotkeys') === false ? i18n.t('enableHotkeys') : i18n.t('disableHotkeys')} 「${i18n.t('globalSetting')}」`, fn: () => { const confirm = window.confirm(`${configManager.getGlobalStorage('enableHotkeys') === false ? i18n.t('enableHotkeys') : i18n.t('disableHotkeys')} 「${i18n.t('globalSetting')}」`); if (confirm) { configManager.setGlobalStorage('enableHotkeys', !configManager.getGlobalStorage('enableHotkeys')); window.location.reload(); } } }, toggleHotkeysStatusUnderCurrentSite: { title: () => `${configManager.getLocalStorage('enableHotkeys') === false ? i18n.t('enableHotkeys') : i18n.t('disableHotkeys')} 「${i18n.t('localSetting')}」`, desc: () => `${configManager.getLocalStorage('enableHotkeys') === false ? i18n.t('enableHotkeys') : i18n.t('disableHotkeys')} 「${i18n.t('localSetting')}」`, fn: () => { const confirm = window.confirm(`${configManager.getLocalStorage('enableHotkeys') === false ? i18n.t('enableHotkeys') : i18n.t('disableHotkeys')} 「${i18n.t('localSetting')}」`); if (confirm) { configManager.setLocalStorage('enableHotkeys', !configManager.getLocalStorage('enableHotkeys')); window.location.reload(); } } }, setLanguage: { title: `${i18n.t('languageSettings')}「${i18n.t('globalSetting')}」`, desc: `${i18n.t('languageSettings')}「${i18n.t('globalSetting')}」`, fn: (lang) => { const confirm = window.confirm(`${i18n.t('languageSettings')}[${lang}] ?`); if (confirm) { if (lang === 'auto' || i18n.languages()[lang]) { configManager.setGlobalStorage('language', lang); window.location.reload(); } else { alert('Language not found'); } } } }, cleanRemoteHelperInfo: { title: i18n.t('cleanRemoteHelperInfo'), desc: i18n.t('cleanRemoteHelperInfo'), fn: () => { configManager.setGlobalStorage('recommendList', false); configManager.setGlobalStorage('contactRemoteHelperSuccessTime', false); configManager.setGlobalStorage('lastContactRemoteHelperTime', false); window.location.reload(); } } }; /*! * @name menuManager.js * @description 菜单管理器 * @version 0.0.1 * @author xxxily * @date 2022/08/11 10:05 * @github https://github.com/xxxily */ let monkeyMenuList = [ { ...globalFunctional.openWebsite }, // { ...globalFunctional.openHotkeysPage }, { ...globalFunctional.openIssuesPage, disable: !configManager.get('enhance.unfoldMenu') }, { ...globalFunctional.openDonatePage }, { ...globalFunctional.toggleScriptEnableState, disable: configManager.get('enable') !== false }, { ...globalFunctional.toggleGUIStatusUnderCurrentSite, disable: configManager.getLocalStorage('ui.enable') !== false }, { ...globalFunctional.toggleGUIStatus, disable: configManager.getGlobalStorage('ui.enable') === false ? false : !configManager.get('enhance.unfoldMenu') }, { ...globalFunctional.toggleHotkeysStatusUnderCurrentSite, disable: configManager.getLocalStorage('enableHotkeys') !== false }, { ...globalFunctional.toggleHotkeysStatus, disable: configManager.get('enableHotkeys') !== false }, { ...globalFunctional.openCustomConfigurationEditor }, /* 展开或收起菜单 */ { ...globalFunctional.toggleExpandedOrCollapsedStateOfMonkeyMenu }, { ...globalFunctional.restoreGlobalConfiguration, disable: !configManager.get('enhance.unfoldMenu') } ]; /* 菜单构造函数(必须是函数才能在点击后动态更新菜单状态) */ function menuBuilder () { return monkeyMenuList } /* 注册动态菜单 */ function menuRegister () { monkeyMenu.build(menuBuilder); } /** * 增加菜单项 * @param {Object|Array} menuOpts 菜单的配置项目,多个配置项目用数组表示 */ function addMenu (menuOpts, before) { menuOpts = Array.isArray(menuOpts) ? menuOpts : [menuOpts]; menuOpts = menuOpts.filter(item => item.title && !item.disabled); if (before) { /* 将菜单追加到其它菜单的前面 */ monkeyMenuList = menuOpts.concat(monkeyMenuList); } else { monkeyMenuList = monkeyMenuList.concat(menuOpts); } /* 重新注册菜单 */ menuRegister(); } /** * 注册跟h5player相关的菜单,只有检测到存在媒体标签了才会注册 */ function registerH5playerMenus (h5player) { const t = h5player; const player = t.player(); const foldMenu = !configManager.get('enhance.unfoldMenu'); if (player && !t._hasRegisterH5playerMenus_) { const menus = [ { ...globalFunctional.openCrossOriginFramePage, disable: foldMenu || !isInCrossOriginFrame() }, { ...globalFunctional.toggleSetCurrentTimeFunctional, type: 'local', disable: foldMenu }, { ...globalFunctional.toggleSetVolumeFunctional, type: 'local', disable: foldMenu }, { ...globalFunctional.toggleSetPlaybackRateFunctional, type: 'global', disable: foldMenu }, { ...globalFunctional.toggleAcousticGainFunctional, type: 'global', disable: foldMenu }, { ...globalFunctional.toggleCrossOriginControlFunctional, type: 'global', disable: foldMenu }, { ...globalFunctional.toggleExperimentFeatures, type: 'global', disable: foldMenu }, { ...globalFunctional.toggleExternalCustomConfiguration, type: 'global', disable: foldMenu }, { ...globalFunctional.toggleDebugMode, disable: foldMenu } ]; let titlePrefix = ''; if (isInIframe()) { titlePrefix = `[${location.hostname}]`; /* 补充title前缀 */ menus.forEach(menu => { const titleFn = menu.title; if (titleFn instanceof Function && menu.type === 'local') { menu.title = () => titlePrefix + titleFn(); } }); } addMenu(menus); t._hasRegisterH5playerMenus_ = true; } } /** * 代理视频播放器的事件注册和取消注册的函数,以对注册事件进行调试或阻断 * @param {*} player * @returns */ 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') { /* 对注册了ratechange事件进行检测,如果存在异常行为,则尝试挂起事件 */ args[1] = new Proxy(listener, { apply (target, ctx, args) { if (ctx) { /* 阻止调速检测,并进行反阻止 */ if (ctx.playbackRate && eventName === 'ratechange') { if (ctx._hasBlockRatechangeEvent_) { return true } const oldRate = ctx.playbackRate; const startTime = Date.now(); const result = target.apply(ctx, args); /** * 通过判断执行ratechange前后的速率是否被改变, * 以及是否出现了超长的执行时间(可能出现了alert弹窗)来检测是否可能存在阻止调速的行为 * 其他检测手段待补充 */ const blockRatechangeBehave1 = oldRate !== ctx.playbackRate || Date.now() - startTime > 1000; const blockRatechangeBehave2 = ctx._setPlaybackRate_ && ctx._setPlaybackRate_.value !== ctx.playbackRate; if (blockRatechangeBehave1 || blockRatechangeBehave2) { debug.info(`[execVideoEvent][${eventName}]检测到可能存在阻止调速的行为,已禁止${eventName}事件的执行`, listener); ctx._hasBlockRatechangeEvent_ = true; return true } else { return result } } } try { return target.apply(ctx, args) } catch (e) { debug.error(`[proxyPlayerEvent][${eventName}]`, listener, e); } } }); } return target.apply(ctx, args) } }); } const mediaSource = (function () { let hasMediaSourceInit = false; const originMethods = {}; const originURLMethods = {}; const mediaSourceMap = new original.Map(); const objectURLMap = new original.Map(); function connectMediaSourceWithMediaElement (mediaEl) { const curSrc = mediaEl.currentSrc || mediaEl.src; if (!curSrc) { return false } mediaSourceMap.forEach(mediaSourceInfo => { if (mediaSourceInfo.mediaSource.__objURL__ && curSrc === mediaSourceInfo.mediaSource.__objURL__) { mediaSourceInfo.mediaElement = mediaEl; } }); } /* 如果mediaSourceMap中关联的mediaEl检测到不存在了,则清理mediaSourceMap中的数据,减少内存占用 */ function cleanMediaSourceData () { function removeMediaSourceData (mediaSourceInfo) { console.log('[cleanMediaSourceData][removeMediaSourceData]', mediaSourceInfo.mediaUrl || mediaSourceInfo.mediaSource.__objURL__); original.map.delete.call(mediaSourceMap, mediaSourceInfo.mediaSource); original.map.delete.call(objectURLMap, mediaSourceInfo.mediaSource); } mediaSourceMap.forEach((mediaSourceInfo) => { if (!mediaSourceInfo.mediaElement || !(mediaSourceInfo.mediaElement instanceof HTMLMediaElement)) { removeMediaSourceData(mediaSourceInfo); } else { if (isOutOfDocument(mediaSourceInfo.mediaElement)) { removeMediaSourceData(mediaSourceInfo); } } }); } function proxyMediaSourceMethod () { if (!originMethods.addSourceBuffer || !originMethods.endOfStream) { return false } // TODO 该代理在上层调用生效可能存在延迟,原因待研究 originURLMethods.createObjectURL = originURLMethods.createObjectURL || URL.prototype.constructor.createObjectURL; URL.prototype.constructor.createObjectURL = new original.Proxy(originURLMethods.createObjectURL, { apply (target, ctx, args) { const object = args[0]; const objectURL = target.apply(ctx, args); if (object instanceof MediaSource && !original.map.has.call(objectURLMap, object)) { object.__objURL__ = objectURL; original.map.set.call(objectURLMap, object, objectURL); } return objectURL } }); MediaSource.prototype.addSourceBuffer = new original.Proxy(originMethods.addSourceBuffer, { apply (target, ctx, args) { if (!original.map.has.call(mediaSourceMap, ctx)) { original.map.set.call(mediaSourceMap, ctx, { mediaSource: ctx, createTime: Date.now(), sourceBuffer: [], endOfStream: false }); } const mediaSourceInfo = original.map.get.call(mediaSourceMap, ctx); const mimeCodecs = args[0] || ''; const sourceBuffer = target.apply(ctx, args); const sourceBufferItem = { mimeCodecs, originAppendBuffer: sourceBuffer.appendBuffer, bufferData: [], mediaInfo: {} }; try { // mimeCodecs字符串示例:'video/mp4; codecs="avc1.42E01E, mp4a.40.2"' const mediaInfo = sourceBufferItem.mediaInfo; const tmpArr = sourceBufferItem.mimeCodecs.split(';'); mediaInfo.type = tmpArr[0].split('/')[0]; mediaInfo.format = tmpArr[0].split('/')[1]; mediaInfo.codecs = tmpArr[1].trim().replace('codecs=', '').replace(/["']/g, ''); } catch (e) { original.console.error('[addSourceBuffer][mediaInfo] 媒体信息解析出错', sourceBufferItem, e); } mediaSourceInfo.sourceBuffer.push(sourceBufferItem); /* 代理sourceBuffer.appendBuffer函数,并将buffer存一份到mediaSourceInfo里 */ sourceBuffer.appendBuffer = new original.Proxy(sourceBufferItem.originAppendBuffer, { apply (bufTarget, bufCtx, bufArgs) { const buffer = bufArgs[0]; if (!mediaSourceInfo.endOfStream) { sourceBufferItem.bufferData.push(buffer); } /* 确保mediaUrl的存在和对应 */ if (original.map.get.call(objectURLMap, ctx)) { mediaSourceInfo.mediaUrl = original.map.get.call(objectURLMap, ctx); } /* 如果appendBuffer依然活跃,但对应的mediaSource却被清理了,则尝试重新将数据关联回去 */ if (!original.map.get.call(mediaSourceMap, ctx)) { original.map.set.call(mediaSourceMap, ctx, mediaSourceInfo); } return bufTarget.apply(bufCtx, bufArgs) } }); return sourceBuffer } }); MediaSource.prototype.endOfStream = new original.Proxy(originMethods.endOfStream, { apply (target, ctx, args) { /* 标识当前媒体流已加载完成 */ const mediaSourceInfo = original.map.get.call(mediaSourceMap, ctx); if (mediaSourceInfo) { mediaSourceInfo.endOfStream = true; if (mediaSourceInfo.mediaElement && mediaSourceInfo.autoDownload && !mediaSourceInfo.hasDownload) { downloadMediaSource(mediaSourceInfo.mediaElement); } } return target.apply(ctx, args) } }); } /** * 下载媒体资源,下载代码参考:https://juejin.cn/post/6873267073674379277 */ function downloadMediaSource (mediaEl, title) { // const srcList = mediaEl.srcList || [] const curSrc = mediaEl.currentSrc || mediaEl.src; if (!curSrc) { original.alert(i18n.t('mediaDownload.notSupport')); return false } let hasFindMediaSource = false; mediaSourceMap.forEach(mediaSourceInfo => { const mediaSource = mediaSourceInfo.mediaSource; if (!mediaSource.__objURL__) { console.error('no objURL', mediaSource, mediaSourceInfo); return false } /* 排除非当前媒体元素的媒体流 */ // if (srcList.length > 0 && !srcList.includes(mediaSource.__objURL__)) { // return false // } if (curSrc !== mediaSource.__objURL__) { return false } hasFindMediaSource = true; mediaSourceInfo.mediaElement = mediaEl; // original.console.log('[downloadMediaSource][mediaSourceInfo]', mediaSourceInfo) if (mediaSourceInfo.hasDownload) { const confirm = original.confirm(i18n.t('mediaDownload.hasDownload')); if (!confirm) { return false } } if (!mediaSourceInfo.hasDownload && !mediaSourceInfo.endOfStream) { // original.console.log('[downloadMediaSource] 媒体数据还没完全就绪', mediaSourceInfo) const confirm = original.confirm(i18n.t('mediaDownload.notEndOfStream')); if (!confirm) { if (mediaSourceInfo.autoDownload) { const cancelAutoDownload = original.confirm(i18n.t('mediaDownload.cancelAutoDownload')); if (cancelAutoDownload) { mediaSourceInfo.autoDownload = false; } } else { const autoDownload = original.confirm(i18n.t('mediaDownload.autoDownload')); if (autoDownload) { mediaSourceInfo.autoDownload = true; } } return false } } let mediaSourceTitle = null; mediaSourceInfo.sourceBuffer.forEach(sourceBufferItem => { if (!sourceBufferItem.mimeCodecs || sourceBufferItem.mimeCodecs.toString().indexOf(';') === -1) { const msg = '[downloadMediaSource][mimeCodecs][error] mimeCodecs不存在或信息异常,无法下载'; original.console.error(msg, sourceBufferItem); original.alert(msg); return false } try { let mediaTitle = `${mediaSourceTitle || sourceBufferItem.mediaInfo.title || title || mediaEl.getAttribute('data-title') || document.title || Date.now()}`; if (!mediaSourceTitle && !sourceBufferItem.mediaInfo.title) { mediaTitle = original.prompt(i18n.t('mediaDownload.confirmTitle'), mediaTitle); if (!mediaTitle) { return false } sourceBufferItem.mediaInfo.title = mediaTitle; } mediaSourceTitle = mediaTitle; /* 自动补充媒体类型和文件后缀 */ mediaTitle = `${mediaTitle}_${sourceBufferItem.mediaInfo.type}.${sourceBufferItem.mediaInfo.format}`; const a = document.createElement('a'); a.href = URL.createObjectURL(new Blob(sourceBufferItem.bufferData)); a.download = mediaTitle; a.click(); URL.revokeObjectURL(a.href); mediaSourceInfo.hasDownload = true; } catch (e) { mediaSourceInfo.hasDownload = false; const msg = '[downloadMediaSource][error]'; original.console.error(msg, e); original.alert(msg); } }); }); if (!hasFindMediaSource) { original.alert(i18n.t('mediaDownload.notFoundMediaSource')); } } function hasInit () { return hasMediaSourceInit } function init () { if (hasMediaSourceInit) { return false } if (!window.MediaSource) { return false } Object.keys(MediaSource.prototype).forEach(key => { try { if (MediaSource.prototype[key] instanceof Function) { originMethods[key] = MediaSource.prototype[key]; } } catch (e) {} }); proxyMediaSourceMethod(); hasMediaSourceInit = true; } return { init, hasInit, originMethods, originURLMethods, mediaSourceMap, objectURLMap, downloadMediaSource, cleanMediaSourceData, connectMediaSourceWithMediaElement } })(); /*! * @name hotkeysRunner.js * @description 热键运行器,实现类似vscode的热键配置方式 * @version 0.0.1 * @author xxxily * @date 2022/11/23 18:22 * @github https://github.com/xxxily */ const Map$1 = window.Map; const WeakMap$1 = window.WeakMap; function isObj (obj) { return Object.prototype.toString.call(obj) === '[object Object]' } function getValByPath (obj, path) { path = path || ''; const pathArr = path.split('.'); let result = obj; /* 递归提取结果值 */ for (let i = 0; i < pathArr.length; i++) { if (!result) break result = result[pathArr[i]]; } return result } function toArrArgs (args) { return Array.isArray(args) ? args : (typeof args === 'undefined' ? [] : [args]) } function isModifierKey (key) { return [ 'ctrl', 'controlleft', 'controlright', 'shift', 'shiftleft', 'shiftright', 'alt', 'altleft', 'altright', 'meta', 'metaleft', 'metaright', 'capsLock'].includes(key.toLowerCase()) } const keyAlias = { ControlLeft: 'ctrl', ControlRight: 'ctrl', ShiftLeft: 'shift', ShiftRight: 'shift', AltLeft: 'alt', AltRight: 'alt', MetaLeft: 'meta', MetaRight: 'meta' }; const combinationKeysMonitor = (function () { const combinationKeysState = new Map$1(); const hasInit = new WeakMap$1(); function init (win = window) { if (!win || win !== win.self || !win.addEventListener || hasInit.get(win)) { return false } const timers = {}; function activeCombinationKeysState (event) { isModifierKey(event.code) && combinationKeysState.set(event.code, true); } function inactivateCombinationKeysState (event) { if (!(event instanceof KeyboardEvent)) { combinationKeysState.forEach((val, key) => { combinationKeysState.set(key, false); }); return true } /** * combinationKeysState状态必须保留一段时间,否则当外部定义的是keyup事件时候,由于这个先注册也先执行, * 马上更改combinationKeysState状态,会导致后面定义的事件拿到的是未激活组合键的状态 */ if (isModifierKey(event.code)) { clearTimeout(timers[event.code]); timers[event.code] = setTimeout(() => { combinationKeysState.set(event.code, false); }, 50); } } win.addEventListener('keydown', activeCombinationKeysState, true); win.addEventListener('keypress', activeCombinationKeysState, true); win.addEventListener('keyup', inactivateCombinationKeysState, true); win.addEventListener('blur', inactivateCombinationKeysState, true); hasInit.set(win, true); } function getCombinationKeys () { const result = new Map$1(); combinationKeysState.forEach((val, key) => { if (val === true) { result.set(key, val); } }); return result } return { combinationKeysState, getCombinationKeys, init } })(); class HotkeysRunner { constructor (hotkeys, win = window) { this.window = win; this.windowList = [win]; /* Mac和window使用的修饰符是不一样的 */ this.MOD = typeof navigator === 'object' && /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? 'Meta' : 'Ctrl'; // 'Control', 'Shift', 'Alt', 'Meta' this.prevPress = null; this._prevTimer_ = null; this.setHotkeys(hotkeys); combinationKeysMonitor.init(win); } /* 设置其它window对象的组合键监控逻辑 */ setCombinationKeysMonitor (win) { this.window = win; if (!this.windowList.includes(win)) { this.windowList.push(win); } combinationKeysMonitor.init(win); } /* 数据预处理 */ hotkeysPreprocess (hotkeys) { if (!Array.isArray(hotkeys)) { return false } hotkeys.forEach((config) => { if (!isObj(config) || !config.key || typeof config.key !== 'string') { return false } const keyName = config.key.trim().toLowerCase(); const mod = this.MOD.toLowerCase(); /* 增加格式化后的hotkeys数组 */ config.keyBindings = keyName.split(' ').map(press => { const keys = press.split(/\b\+/); const mods = []; let key = ''; keys.forEach((k) => { k = k === '$mod' ? mod : k; if (isModifierKey(k)) { mods.push(k); } else { key = k; } }); return [mods, key] }); }); return hotkeys } setHotkeys (hotkeys) { this.hotkeys = this.hotkeysPreprocess(hotkeys) || []; } /** * 判断当前提供的键盘事件和预期的热键配置是否匹配 * @param {KeyboardEvent} event * @param {Array} press 例如:[['alt', 'shift'], 's'] * @param {Object} prevCombinationKeys * @returns */ isMatch (event, press) { if (!event || !Array.isArray(press)) { return false } const combinationKeys = event.combinationKeys || combinationKeysMonitor.getCombinationKeys(); const mods = press[0]; const key = press[1]; /* 修饰符个数不匹配 */ if (mods.length !== combinationKeys.size) { return false } /* 当前按下的键位和预期的键位不匹配 */ if (key && event.key.toLowerCase() !== key && event.code.toLowerCase() !== key) { return false } /* 当前按下的修饰符和预期的修饰符不匹配 */ let result = true; const modsKey = new Map$1(); combinationKeys.forEach((val, key) => { /* 补充各种可能情况的标识 */ modsKey.set(key, val); modsKey.set(key.toLowerCase(), val); keyAlias[key] && modsKey.set(keyAlias[key], val); }); mods.forEach((key) => { if (!modsKey.has(key)) { result = false; } }); return result } isMatchPrevPress (press) { return this.isMatch(this.prevPress, press) } run (opts = {}) { // 这里只对单个window有效 // const KeyboardEvent = this.window.KeyboardEvent // if (!(opts.event instanceof KeyboardEvent)) { return false } const KeyboardEventList = this.windowList.map(win => win.KeyboardEvent); if (!KeyboardEventList.includes(opts.event.constructor)) { return false } const event = opts.event; const target = opts.target || null; const conditionHandler = opts.conditionHandler || opts.whenHandler; let matchResult = null; this.hotkeys.forEach(hotkeyConf => { if (hotkeyConf.disabled || !hotkeyConf.keyBindings) { return false } let press = hotkeyConf.keyBindings[0]; /* 当存在prevPress,则不再响应与prevPress不匹配的其它快捷键 */ if (this.prevPress && (hotkeyConf.keyBindings.length <= 1 || !this.isMatchPrevPress(press))) { return false } /* 如果存在上一轮的操作快捷键记录,且之前的快捷键与第一个keyBindings定义的快捷键匹配,则去匹配第二个keyBindings */ if (this.prevPress && hotkeyConf.keyBindings.length > 1 && this.isMatchPrevPress(press)) { press = hotkeyConf.keyBindings[1]; } const isMatch = this.isMatch(event, press); if (!isMatch) { return false } matchResult = hotkeyConf; /* 是否阻止事件冒泡和阻止默认事件 */ const stopPropagation = opts.stopPropagation || hotkeyConf.stopPropagation; const preventDefault = opts.preventDefault || hotkeyConf.preventDefault; stopPropagation && event.stopPropagation(); preventDefault && event.preventDefault(); /* 记录上一次操作的快捷键,且一段时间后清空该操作的记录 */ if (press === hotkeyConf.keyBindings[0] && hotkeyConf.keyBindings.length > 1) { /* 将prevPress变成一个具有event相关字段的对象 */ this.prevPress = { combinationKeys: combinationKeysMonitor.getCombinationKeys(), code: event.code, key: event.key, keyCode: event.keyCode, altKey: event.altKey, shiftKey: event.shiftKey, ctrlKey: event.ctrlKey, metaKey: event.metaKey }; clearTimeout(this._prevTimer_); this._prevTimer_ = setTimeout(() => { this.prevPress = null; }, 1000); return true } /* 如果当前匹配到了第二个快捷键,则当forEach循环结束后,马上注销prevPress,给其它快捷键让行 */ if (hotkeyConf.keyBindings.length > 1 && press !== hotkeyConf.keyBindings[0]) { setTimeout(() => { this.prevPress = null; }, 0); } /* 执行hotkeyConf.command对应的函数或命令 */ const args = toArrArgs(hotkeyConf.args); let commandFunc = hotkeyConf.command; if (target && typeof hotkeyConf.command === 'string') { commandFunc = getValByPath(target, hotkeyConf.command); } if (!(commandFunc instanceof Function) && target) { throw new Error(`[hotkeysRunner] 未找到command: ${hotkeyConf.command} 对应的函数`) } if (hotkeyConf.when && conditionHandler instanceof Function) { const isMatchCondition = conditionHandler.apply(target, toArrArgs(hotkeyConf.when)); if (isMatchCondition === true) { commandFunc.apply(target, args); } } else { commandFunc.apply(target, args); } }); return matchResult } binding (opts = {}) { if (!isObj(opts) || !Array.isArray(opts.hotkeys)) { throw new Error('[hotkeysRunner] 提供给binding的参数不正确') } opts.el = opts.el || this.window; opts.type = opts.type || 'keydown'; opts.debug && (this.debug = true); this.setHotkeys(opts.hotkeys); if (typeof opts.el === 'string') { opts.el = document.querySelector(opts.el); } opts.el.addEventListener(opts.type, (event) => { opts.event = event; this.run(opts); }, true); } } /* eslint-disable camelcase */ /** * @license Copyright 2017 - Chris West - MIT Licensed * Prototype to easily set the volume (actual and perceived), loudness, * decibels, and gain value. * https://cwestblog.com/2017/08/22/web-audio-api-controlling-audio-video-loudness/ */ function MediaElementAmplifier (mediaElem) { this._context = new (window.AudioContext || window.webkitAudioContext)(); this._source = this._context.createMediaElementSource(this._element = mediaElem); this._source.connect(this._gain = this._context.createGain()); this._gain.connect(this._context.destination); } [ 'getContext', 'getSource', 'getGain', 'getElement', [ 'getVolume', function (opt_getPerceived) { return (opt_getPerceived ? this.getLoudness() : 1) * this._element.volume } ], [ 'setVolume', function (value, opt_setPerceived) { var volume = value / (opt_setPerceived ? this.getLoudness() : 1); if (volume > 1) { this.setLoudness(this.getLoudness() * volume); volume = 1; } this._element.volume = volume; } ], ['getGainValue', function () { return this._gain.gain.value }], ['setGainValue', function (value) { this._gain.gain.value = value; }], ['getDecibels', function () { return 20 * Math.log10(this.getGainValue()) }], ['setDecibels', function (value) { this.setGainValue(Math.pow(10, value / 20)); }], ['getLoudness', function () { return Math.pow(2, this.getDecibels() / 10) }], ['setLoudness', function (value) { this.setDecibels(10 * Math.log2(value)); }] ].forEach(function (name, fn) { if (typeof name === 'string') { fn = function () { return this[name.replace('get', '').toLowerCase()] }; } else { fn = name[1]; name = name[0]; } MediaElementAmplifier.prototype[name] = fn; }); const downloadState = new Map(); function download (url, title) { const downloadEl = document.createElement('a'); downloadEl.href = url; downloadEl.target = '_blank'; downloadEl.download = title; downloadEl.click(); } function mediaDownload (mediaEl, title, downloadType) { /** * 当媒体包含source标签时,媒体标签的真实地址将会是currentSrc * https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/currentSrc */ const mediaUrl = mediaEl.src || mediaEl.currentSrc; const mediaState = downloadState.get(mediaUrl) || {}; if (mediaEl && mediaUrl && !mediaUrl.startsWith('blob:')) { const mediaInfo = { type: mediaEl instanceof HTMLVideoElement ? 'video' : 'audio', format: mediaEl instanceof HTMLVideoElement ? 'mp4' : 'mp3' }; let mediaTitle = `${title || mediaEl.getAttribute('data-title') || document.title || Date.now()}_${mediaInfo.type}.${mediaInfo.format}`; /* 小于5分钟的媒体文件,尝试通过fetch下载 */ if (downloadType === 'blob' || mediaEl.duration < 60 * 5) { if (mediaState.downloading) { /* 距上次点下载小于1s的情况直接不响应任何操作 */ if (Date.now() - mediaState.downloading < 1000 * 1) { return false } else { const confirm = original.confirm(i18n.t('mediaDownload.downloading')); if (!confirm) { return false } } } if (mediaState.hasDownload) { const confirm = original.confirm(i18n.t('mediaDownload.hasDownload')); if (!confirm) { return false } } mediaTitle = original.prompt(i18n.t('mediaDownload.confirmTitle'), mediaTitle); if (!mediaTitle) { return false } if (!mediaTitle.endsWith(mediaInfo.format)) { mediaTitle = mediaTitle + '.' + mediaInfo.format; } let fetchUrl = mediaUrl; if (mediaUrl.startsWith('http://') && location.href.startsWith('https://')) { /* 在https里fetch http资源会导致 block:mixed-content 错误,所以尝试将地址统一成https开头 */ fetchUrl = mediaUrl.replace('http://', 'https://'); } mediaState.downloading = Date.now(); downloadState.set(mediaUrl, mediaState); fetch(fetchUrl).then(res => { res.blob().then(blob => { const blobUrl = window.URL.createObjectURL(blob); download(blobUrl, mediaTitle); mediaState.hasDownload = true; delete mediaState.downloading; downloadState.set(mediaUrl, mediaState); window.URL.revokeObjectURL(blobUrl); }); }).catch(err => { original.console.error('fetch下载操作失败:', err); /* 下载兜底 */ download(mediaUrl, mediaTitle); delete mediaState.downloading; mediaState.hasDownload = true; downloadState.set(mediaUrl, mediaState); }); } else { download(mediaUrl, mediaTitle); } } else if (mediaSource.hasInit()) { /* 下载通过MediaSource管理的媒体文件 */ mediaSource.downloadMediaSource(mediaEl, title); } else { original.alert(i18n.t('mediaDownload.notSupport')); } } const device = { isMobile: () => { return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) }, isTablet: () => { return /iPad/i.test(navigator.userAgent) }, isDesktop: () => { return !device.isMobile() && !device.isTablet() }, isChrome: () => { return /Chrome/i.test(navigator.userAgent) }, isFirefox: () => { return /Firefox/i.test(navigator.userAgent) }, isSafari: () => { return /Safari/i.test(navigator.userAgent) }, isEdge: () => { return /Edge/i.test(navigator.userAgent) } }; /** * 提供一些跟h5player共享的全局方法,减少重复代码,和共享一些需要提前执行才能获取得到得对象 */ const h5playerUIProvider = { version, originalMethods, parseHTML, observeVisibility, isOutOfDocument, i18n, debug, configManager, globalFunctional, device }; /** * 通过proxy创建个window的沙盒传递给h5playerUiWraper * 目的是可以提供一些干净的全局对象给到h5playerUI * 另外是避免h5playerUI中的代码污染到实际的window对象 */ const windowSandbox = new Proxy({}, { get: function (target, key) { if (key === 'h5playerUIProvider') { return h5playerUIProvider } if (key === 'HTMLElement') { return originalMethods.HTMLElement } return window[key] } }); /** * 跟官网进行互动,以实现以下功能 * 1、新版本检测 (待实现) * 2、脚本安装使用情况统计 * 3、获取最新的推荐信息 */ const remoteHelperUrl = 'https://h5player.anzz.top/h5p-helper/index.html'; const remoteHelper = { init () { this.remoteHandler(); /* 减少重复加载和防止循环嵌套 */ if (isInIframe()) { return false } if (!configManager.isGlobalStorageUsable()) { return false } const contactRemoteHelperSuccessTime = configManager.getGlobalStorage('contactRemoteHelperSuccessTime'); let lastContactRemoteHelperTime = configManager.getGlobalStorage('lastContactRemoteHelperTime'); if (!lastContactRemoteHelperTime) { configManager.setGlobalStorage('lastContactRemoteHelperTime', Date.now()); lastContactRemoteHelperTime = Date.now(); } /** * 减少跟远程助手的握手次数 * 12小时内有成功握手过的话,就不再重复握手 * 最少间隔1分钟才进行下一次握手 */ const syncInterval = configManager.getGlobalStorage('remoteHelperSyncInterval') || 1000 * 60 * 60 * 12; if (contactRemoteHelperSuccessTime && Date.now() - contactRemoteHelperSuccessTime < syncInterval) { return false } if (Date.now() - lastContactRemoteHelperTime < 1000 * 60) { return false } this.establishRemoteConnection(); }, establishRemoteConnection () { const lastSucTime = configManager.getGlobalStorage('contactRemoteHelperSuccessTime') || '0'; const timeStr = new Date().toISOString().split('T')[0].replace(/-/g, '') + new Date().getHours() + '' + new Date().getMinutes(); const iframe = document.createElement('iframe'); iframe.src = `${remoteHelperUrl}?t=${timeStr}&v=${version}&lst=${lastSucTime}`; iframe.style.cssText = 'width:0; height:0; border:none; visibility:hidden; opacity:0;'; const insertIframe = () => { document.body.appendChild(iframe); configManager.setGlobalStorage('lastContactRemoteHelperTime', Date.now()); }; if (!document.body || !document.body.appendChild) { window.addEventListener('DOMContentLoaded', insertIframe, { once: true }); } else { insertIframe(); } /* 不管握手成功与否,10秒后移除iframe,主动终止跟远程助手的连接 */ setTimeout(() => { document.body.removeChild(iframe); }, 10000); }, async remoteHandler () { if (!location.href.startsWith(remoteHelperUrl) || !configManager.isGlobalStorageUsable()) { return false } function syncRemoteData (pageWindow) { if (pageWindow.recommendList) { configManager.setGlobalStorage('recommendList', pageWindow.recommendList); } /* 待增加版本对比判断逻辑 */ if (pageWindow.remoteVersion) { configManager.setGlobalStorage('remoteVersion', pageWindow.remoteVersion); } if (pageWindow.remoteHelperSyncInterval) { configManager.setGlobalStorage('remoteHelperSyncInterval', pageWindow.remoteHelperSyncInterval); } configManager.setGlobalStorage('contactRemoteHelperSuccessTime', Date.now()); } let checkCount = 0; function checkRemoteHelperStatus (pageWindow) { if (!Array.isArray(pageWindow.recommendList)) { if (checkCount < 30) { setTimeout(() => { checkCount++; checkRemoteHelperStatus(pageWindow); }, 200); } return } syncRemoteData(pageWindow); } const pageWindow = await getPageWindow(); pageWindow && checkRemoteHelperStatus(pageWindow); } }; const h5playerUI = function (window) {var h5playerUI = (function () { const sheet = new CSSStyleSheet();sheet.replaceSync(":root,\n:host,\n.sl-theme-light {\n color-scheme: light;\n\n --sl-color-gray-50: hsl(0 0% 97.5%);\n --sl-color-gray-100: hsl(240 4.8% 95.9%);\n --sl-color-gray-200: hsl(240 5.9% 90%);\n --sl-color-gray-300: hsl(240 4.9% 83.9%);\n --sl-color-gray-400: hsl(240 5% 64.9%);\n --sl-color-gray-500: hsl(240 3.8% 46.1%);\n --sl-color-gray-600: hsl(240 5.2% 33.9%);\n --sl-color-gray-700: hsl(240 5.3% 26.1%);\n --sl-color-gray-800: hsl(240 3.7% 15.9%);\n --sl-color-gray-900: hsl(240 5.9% 10%);\n --sl-color-gray-950: hsl(240 7.3% 8%);\n\n --sl-color-red-50: hsl(0 85.7% 97.3%);\n --sl-color-red-100: hsl(0 93.3% 94.1%);\n --sl-color-red-200: hsl(0 96.3% 89.4%);\n --sl-color-red-300: hsl(0 93.5% 81.8%);\n --sl-color-red-400: hsl(0 90.6% 70.8%);\n --sl-color-red-500: hsl(0 84.2% 60.2%);\n --sl-color-red-600: hsl(0 72.2% 50.6%);\n --sl-color-red-700: hsl(0 73.7% 41.8%);\n --sl-color-red-800: hsl(0 70% 35.3%);\n --sl-color-red-900: hsl(0 62.8% 30.6%);\n --sl-color-red-950: hsl(0 60% 19.6%);\n\n --sl-color-orange-50: hsl(33.3 100% 96.5%);\n --sl-color-orange-100: hsl(34.3 100% 91.8%);\n --sl-color-orange-200: hsl(32.1 97.7% 83.1%);\n --sl-color-orange-300: hsl(30.7 97.2% 72.4%);\n --sl-color-orange-400: hsl(27 96% 61%);\n --sl-color-orange-500: hsl(24.6 95% 53.1%);\n --sl-color-orange-600: hsl(20.5 90.2% 48.2%);\n --sl-color-orange-700: hsl(17.5 88.3% 40.4%);\n --sl-color-orange-800: hsl(15 79.1% 33.7%);\n --sl-color-orange-900: hsl(15.3 74.6% 27.8%);\n --sl-color-orange-950: hsl(15.2 69.1% 19%);\n\n --sl-color-amber-50: hsl(48 100% 96.1%);\n --sl-color-amber-100: hsl(48 96.5% 88.8%);\n --sl-color-amber-200: hsl(48 96.6% 76.7%);\n --sl-color-amber-300: hsl(45.9 96.7% 64.5%);\n --sl-color-amber-400: hsl(43.3 96.4% 56.3%);\n --sl-color-amber-500: hsl(37.7 92.1% 50.2%);\n --sl-color-amber-600: hsl(32.1 94.6% 43.7%);\n --sl-color-amber-700: hsl(26 90.5% 37.1%);\n --sl-color-amber-800: hsl(22.7 82.5% 31.4%);\n --sl-color-amber-900: hsl(21.7 77.8% 26.5%);\n --sl-color-amber-950: hsl(22.9 74.1% 16.7%);\n\n --sl-color-yellow-50: hsl(54.5 91.7% 95.3%);\n --sl-color-yellow-100: hsl(54.9 96.7% 88%);\n --sl-color-yellow-200: hsl(52.8 98.3% 76.9%);\n --sl-color-yellow-300: hsl(50.4 97.8% 63.5%);\n --sl-color-yellow-400: hsl(47.9 95.8% 53.1%);\n --sl-color-yellow-500: hsl(45.4 93.4% 47.5%);\n --sl-color-yellow-600: hsl(40.6 96.1% 40.4%);\n --sl-color-yellow-700: hsl(35.5 91.7% 32.9%);\n --sl-color-yellow-800: hsl(31.8 81% 28.8%);\n --sl-color-yellow-900: hsl(28.4 72.5% 25.7%);\n --sl-color-yellow-950: hsl(33.1 69% 13.9%);\n\n --sl-color-lime-50: hsl(78.3 92% 95.1%);\n --sl-color-lime-100: hsl(79.6 89.1% 89.2%);\n --sl-color-lime-200: hsl(80.9 88.5% 79.6%);\n --sl-color-lime-300: hsl(82 84.5% 67.1%);\n --sl-color-lime-400: hsl(82.7 78% 55.5%);\n --sl-color-lime-500: hsl(83.7 80.5% 44.3%);\n --sl-color-lime-600: hsl(84.8 85.2% 34.5%);\n --sl-color-lime-700: hsl(85.9 78.4% 27.3%);\n --sl-color-lime-800: hsl(86.3 69% 22.7%);\n --sl-color-lime-900: hsl(87.6 61.2% 20.2%);\n --sl-color-lime-950: hsl(86.5 60.6% 13.9%);\n\n --sl-color-green-50: hsl(138.5 76.5% 96.7%);\n --sl-color-green-100: hsl(140.6 84.2% 92.5%);\n --sl-color-green-200: hsl(141 78.9% 85.1%);\n --sl-color-green-300: hsl(141.7 76.6% 73.1%);\n --sl-color-green-400: hsl(141.9 69.2% 58%);\n --sl-color-green-500: hsl(142.1 70.6% 45.3%);\n --sl-color-green-600: hsl(142.1 76.2% 36.3%);\n --sl-color-green-700: hsl(142.4 71.8% 29.2%);\n --sl-color-green-800: hsl(142.8 64.2% 24.1%);\n --sl-color-green-900: hsl(143.8 61.2% 20.2%);\n --sl-color-green-950: hsl(144.3 60.7% 12%);\n\n --sl-color-emerald-50: hsl(151.8 81% 95.9%);\n --sl-color-emerald-100: hsl(149.3 80.4% 90%);\n --sl-color-emerald-200: hsl(152.4 76% 80.4%);\n --sl-color-emerald-300: hsl(156.2 71.6% 66.9%);\n --sl-color-emerald-400: hsl(158.1 64.4% 51.6%);\n --sl-color-emerald-500: hsl(160.1 84.1% 39.4%);\n --sl-color-emerald-600: hsl(161.4 93.5% 30.4%);\n --sl-color-emerald-700: hsl(162.9 93.5% 24.3%);\n --sl-color-emerald-800: hsl(163.1 88.1% 19.8%);\n --sl-color-emerald-900: hsl(164.2 85.7% 16.5%);\n --sl-color-emerald-950: hsl(164.3 87.5% 9.4%);\n\n --sl-color-teal-50: hsl(166.2 76.5% 96.7%);\n --sl-color-teal-100: hsl(167.2 85.5% 89.2%);\n --sl-color-teal-200: hsl(168.4 83.8% 78.2%);\n --sl-color-teal-300: hsl(170.6 76.9% 64.3%);\n --sl-color-teal-400: hsl(172.5 66% 50.4%);\n --sl-color-teal-500: hsl(173.4 80.4% 40%);\n --sl-color-teal-600: hsl(174.7 83.9% 31.6%);\n --sl-color-teal-700: hsl(175.3 77.4% 26.1%);\n --sl-color-teal-800: hsl(176.1 69.4% 21.8%);\n --sl-color-teal-900: hsl(175.9 60.8% 19%);\n --sl-color-teal-950: hsl(176.5 58.6% 11.4%);\n\n --sl-color-cyan-50: hsl(183.2 100% 96.3%);\n --sl-color-cyan-100: hsl(185.1 95.9% 90.4%);\n --sl-color-cyan-200: hsl(186.2 93.5% 81.8%);\n --sl-color-cyan-300: hsl(187 92.4% 69%);\n --sl-color-cyan-400: hsl(187.9 85.7% 53.3%);\n --sl-color-cyan-500: hsl(188.7 94.5% 42.7%);\n --sl-color-cyan-600: hsl(191.6 91.4% 36.5%);\n --sl-color-cyan-700: hsl(192.9 82.3% 31%);\n --sl-color-cyan-800: hsl(194.4 69.6% 27.1%);\n --sl-color-cyan-900: hsl(196.4 63.6% 23.7%);\n --sl-color-cyan-950: hsl(196.8 61% 16.1%);\n\n --sl-color-sky-50: hsl(204 100% 97.1%);\n --sl-color-sky-100: hsl(204 93.8% 93.7%);\n --sl-color-sky-200: hsl(200.6 94.4% 86.1%);\n --sl-color-sky-300: hsl(199.4 95.5% 73.9%);\n --sl-color-sky-400: hsl(198.4 93.2% 59.6%);\n --sl-color-sky-500: hsl(198.6 88.7% 48.4%);\n --sl-color-sky-600: hsl(200.4 98% 39.4%);\n --sl-color-sky-700: hsl(201.3 96.3% 32.2%);\n --sl-color-sky-800: hsl(201 90% 27.5%);\n --sl-color-sky-900: hsl(202 80.3% 23.9%);\n --sl-color-sky-950: hsl(202.3 73.8% 16.5%);\n\n --sl-color-blue-50: hsl(213.8 100% 96.9%);\n --sl-color-blue-100: hsl(214.3 94.6% 92.7%);\n --sl-color-blue-200: hsl(213.3 96.9% 87.3%);\n --sl-color-blue-300: hsl(211.7 96.4% 78.4%);\n --sl-color-blue-400: hsl(213.1 93.9% 67.8%);\n --sl-color-blue-500: hsl(217.2 91.2% 59.8%);\n --sl-color-blue-600: hsl(221.2 83.2% 53.3%);\n --sl-color-blue-700: hsl(224.3 76.3% 48%);\n --sl-color-blue-800: hsl(225.9 70.7% 40.2%);\n --sl-color-blue-900: hsl(224.4 64.3% 32.9%);\n --sl-color-blue-950: hsl(226.2 55.3% 18.4%);\n\n --sl-color-indigo-50: hsl(225.9 100% 96.7%);\n --sl-color-indigo-100: hsl(226.5 100% 93.9%);\n --sl-color-indigo-200: hsl(228 96.5% 88.8%);\n --sl-color-indigo-300: hsl(229.7 93.5% 81.8%);\n --sl-color-indigo-400: hsl(234.5 89.5% 73.9%);\n --sl-color-indigo-500: hsl(238.7 83.5% 66.7%);\n --sl-color-indigo-600: hsl(243.4 75.4% 58.6%);\n --sl-color-indigo-700: hsl(244.5 57.9% 50.6%);\n --sl-color-indigo-800: hsl(243.7 54.5% 41.4%);\n --sl-color-indigo-900: hsl(242.2 47.4% 34.3%);\n --sl-color-indigo-950: hsl(243.5 43.6% 22.9%);\n\n --sl-color-violet-50: hsl(250 100% 97.6%);\n --sl-color-violet-100: hsl(251.4 91.3% 95.5%);\n --sl-color-violet-200: hsl(250.5 95.2% 91.8%);\n --sl-color-violet-300: hsl(252.5 94.7% 85.1%);\n --sl-color-violet-400: hsl(255.1 91.7% 76.3%);\n --sl-color-violet-500: hsl(258.3 89.5% 66.3%);\n --sl-color-violet-600: hsl(262.1 83.3% 57.8%);\n --sl-color-violet-700: hsl(263.4 70% 50.4%);\n --sl-color-violet-800: hsl(263.4 69.3% 42.2%);\n --sl-color-violet-900: hsl(263.5 67.4% 34.9%);\n --sl-color-violet-950: hsl(265.1 61.5% 21.4%);\n\n --sl-color-purple-50: hsl(270 100% 98%);\n --sl-color-purple-100: hsl(268.7 100% 95.5%);\n --sl-color-purple-200: hsl(268.6 100% 91.8%);\n --sl-color-purple-300: hsl(269.2 97.4% 85.1%);\n --sl-color-purple-400: hsl(270 95.2% 75.3%);\n --sl-color-purple-500: hsl(270.7 91% 65.1%);\n --sl-color-purple-600: hsl(271.5 81.3% 55.9%);\n --sl-color-purple-700: hsl(272.1 71.7% 47.1%);\n --sl-color-purple-800: hsl(272.9 67.2% 39.4%);\n --sl-color-purple-900: hsl(273.6 65.6% 32%);\n --sl-color-purple-950: hsl(276 59.5% 16.5%);\n\n --sl-color-fuchsia-50: hsl(289.1 100% 97.8%);\n --sl-color-fuchsia-100: hsl(287 100% 95.5%);\n --sl-color-fuchsia-200: hsl(288.3 95.8% 90.6%);\n --sl-color-fuchsia-300: hsl(291.1 93.1% 82.9%);\n --sl-color-fuchsia-400: hsl(292 91.4% 72.5%);\n --sl-color-fuchsia-500: hsl(292.2 84.1% 60.6%);\n --sl-color-fuchsia-600: hsl(293.4 69.5% 48.8%);\n --sl-color-fuchsia-700: hsl(294.7 72.4% 39.8%);\n --sl-color-fuchsia-800: hsl(295.4 70.2% 32.9%);\n --sl-color-fuchsia-900: hsl(296.7 63.6% 28%);\n --sl-color-fuchsia-950: hsl(297.1 56.8% 14.5%);\n\n --sl-color-pink-50: hsl(327.3 73.3% 97.1%);\n --sl-color-pink-100: hsl(325.7 77.8% 94.7%);\n --sl-color-pink-200: hsl(325.9 84.6% 89.8%);\n --sl-color-pink-300: hsl(327.4 87.1% 81.8%);\n --sl-color-pink-400: hsl(328.6 85.5% 70.2%);\n --sl-color-pink-500: hsl(330.4 81.2% 60.4%);\n --sl-color-pink-600: hsl(333.3 71.4% 50.6%);\n --sl-color-pink-700: hsl(335.1 77.6% 42%);\n --sl-color-pink-800: hsl(335.8 74.4% 35.3%);\n --sl-color-pink-900: hsl(335.9 69% 30.4%);\n --sl-color-pink-950: hsl(336.2 65.4% 15.9%);\n\n --sl-color-rose-50: hsl(355.7 100% 97.3%);\n --sl-color-rose-100: hsl(355.6 100% 94.7%);\n --sl-color-rose-200: hsl(352.7 96.1% 90%);\n --sl-color-rose-300: hsl(352.6 95.7% 81.8%);\n --sl-color-rose-400: hsl(351.3 94.5% 71.4%);\n --sl-color-rose-500: hsl(349.7 89.2% 60.2%);\n --sl-color-rose-600: hsl(346.8 77.2% 49.8%);\n --sl-color-rose-700: hsl(345.3 82.7% 40.8%);\n --sl-color-rose-800: hsl(343.4 79.7% 34.7%);\n --sl-color-rose-900: hsl(341.5 75.5% 30.4%);\n --sl-color-rose-950: hsl(341.3 70.1% 17.1%);\n\n --sl-color-primary-50: var(--sl-color-sky-50);\n --sl-color-primary-100: var(--sl-color-sky-100);\n --sl-color-primary-200: var(--sl-color-sky-200);\n --sl-color-primary-300: var(--sl-color-sky-300);\n --sl-color-primary-400: var(--sl-color-sky-400);\n --sl-color-primary-500: var(--sl-color-sky-500);\n --sl-color-primary-600: var(--sl-color-sky-600);\n --sl-color-primary-700: var(--sl-color-sky-700);\n --sl-color-primary-800: var(--sl-color-sky-800);\n --sl-color-primary-900: var(--sl-color-sky-900);\n --sl-color-primary-950: var(--sl-color-sky-950);\n\n --sl-color-success-50: var(--sl-color-green-50);\n --sl-color-success-100: var(--sl-color-green-100);\n --sl-color-success-200: var(--sl-color-green-200);\n --sl-color-success-300: var(--sl-color-green-300);\n --sl-color-success-400: var(--sl-color-green-400);\n --sl-color-success-500: var(--sl-color-green-500);\n --sl-color-success-600: var(--sl-color-green-600);\n --sl-color-success-700: var(--sl-color-green-700);\n --sl-color-success-800: var(--sl-color-green-800);\n --sl-color-success-900: var(--sl-color-green-900);\n --sl-color-success-950: var(--sl-color-green-950);\n\n --sl-color-warning-50: var(--sl-color-amber-50);\n --sl-color-warning-100: var(--sl-color-amber-100);\n --sl-color-warning-200: var(--sl-color-amber-200);\n --sl-color-warning-300: var(--sl-color-amber-300);\n --sl-color-warning-400: var(--sl-color-amber-400);\n --sl-color-warning-500: var(--sl-color-amber-500);\n --sl-color-warning-600: var(--sl-color-amber-600);\n --sl-color-warning-700: var(--sl-color-amber-700);\n --sl-color-warning-800: var(--sl-color-amber-800);\n --sl-color-warning-900: var(--sl-color-amber-900);\n --sl-color-warning-950: var(--sl-color-amber-950);\n\n --sl-color-danger-50: var(--sl-color-red-50);\n --sl-color-danger-100: var(--sl-color-red-100);\n --sl-color-danger-200: var(--sl-color-red-200);\n --sl-color-danger-300: var(--sl-color-red-300);\n --sl-color-danger-400: var(--sl-color-red-400);\n --sl-color-danger-500: var(--sl-color-red-500);\n --sl-color-danger-600: var(--sl-color-red-600);\n --sl-color-danger-700: var(--sl-color-red-700);\n --sl-color-danger-800: var(--sl-color-red-800);\n --sl-color-danger-900: var(--sl-color-red-900);\n --sl-color-danger-950: var(--sl-color-red-950);\n\n --sl-color-neutral-50: var(--sl-color-gray-50);\n --sl-color-neutral-100: var(--sl-color-gray-100);\n --sl-color-neutral-200: var(--sl-color-gray-200);\n --sl-color-neutral-300: var(--sl-color-gray-300);\n --sl-color-neutral-400: var(--sl-color-gray-400);\n --sl-color-neutral-500: var(--sl-color-gray-500);\n --sl-color-neutral-600: var(--sl-color-gray-600);\n --sl-color-neutral-700: var(--sl-color-gray-700);\n --sl-color-neutral-800: var(--sl-color-gray-800);\n --sl-color-neutral-900: var(--sl-color-gray-900);\n --sl-color-neutral-950: var(--sl-color-gray-950);\n\n --sl-color-neutral-0: hsl(0, 0%, 100%);\n --sl-color-neutral-1000: hsl(0, 0%, 0%);\n\n --sl-border-radius-small: 0.1875rem;\n --sl-border-radius-medium: 0.25rem;\n --sl-border-radius-large: 0.5rem;\n --sl-border-radius-x-large: 1rem;\n\n --sl-border-radius-circle: 50%;\n --sl-border-radius-pill: 9999px;\n\n --sl-shadow-x-small: 0 1px 2px hsl(240 3.8% 46.1% / 6%);\n --sl-shadow-small: 0 1px 2px hsl(240 3.8% 46.1% / 12%);\n --sl-shadow-medium: 0 2px 4px hsl(240 3.8% 46.1% / 12%);\n --sl-shadow-large: 0 2px 8px hsl(240 3.8% 46.1% / 12%);\n --sl-shadow-x-large: 0 4px 16px hsl(240 3.8% 46.1% / 12%);\n\n --sl-spacing-3x-small: 0.125rem;\n --sl-spacing-2x-small: 0.25rem;\n --sl-spacing-x-small: 0.5rem;\n --sl-spacing-small: 0.75rem;\n --sl-spacing-medium: 1rem;\n --sl-spacing-large: 1.25rem;\n --sl-spacing-x-large: 1.75rem;\n --sl-spacing-2x-large: 2.25rem;\n --sl-spacing-3x-large: 3rem;\n --sl-spacing-4x-large: 4.5rem;\n\n --sl-transition-x-slow: 1000ms;\n --sl-transition-slow: 500ms;\n --sl-transition-medium: 250ms;\n --sl-transition-fast: 150ms;\n --sl-transition-x-fast: 50ms;\n\n --sl-font-mono: SFMono-Regular, Consolas, \"Liberation Mono\", Menlo, monospace;\n --sl-font-sans: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto,\n Helvetica, Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\",\n \"Segoe UI Symbol\";\n --sl-font-serif: Georgia, \"Times New Roman\", serif;\n\n --sl-font-size-2x-small: 0.625rem;\n --sl-font-size-x-small: 0.75rem;\n --sl-font-size-small: 0.875rem;\n --sl-font-size-medium: 1rem;\n --sl-font-size-large: 1.25rem;\n --sl-font-size-x-large: 1.5rem;\n --sl-font-size-2x-large: 2.25rem;\n --sl-font-size-3x-large: 3rem;\n --sl-font-size-4x-large: 4.5rem;\n\n --sl-font-weight-light: 300;\n --sl-font-weight-normal: 400;\n --sl-font-weight-semibold: 500;\n --sl-font-weight-bold: 700;\n\n --sl-letter-spacing-denser: -0.03em;\n --sl-letter-spacing-dense: -0.015em;\n --sl-letter-spacing-normal: normal;\n --sl-letter-spacing-loose: 0.075em;\n --sl-letter-spacing-looser: 0.15em;\n\n --sl-line-height-denser: 1;\n --sl-line-height-dense: 1.4;\n --sl-line-height-normal: 1.8;\n --sl-line-height-loose: 2.2;\n --sl-line-height-looser: 2.6;\n\n --sl-focus-ring-color: var(--sl-color-primary-600);\n --sl-focus-ring-style: solid;\n --sl-focus-ring-width: 3px;\n --sl-focus-ring: var(--sl-focus-ring-style) var(--sl-focus-ring-width)\n var(--sl-focus-ring-color);\n --sl-focus-ring-offset: 1px;\n\n --sl-button-font-size-small: var(--sl-font-size-x-small);\n --sl-button-font-size-medium: var(--sl-font-size-small);\n --sl-button-font-size-large: var(--sl-font-size-medium);\n\n --sl-input-height-small: 1.875rem;\n --sl-input-height-medium: 2.5rem;\n --sl-input-height-large: 3.125rem;\n\n --sl-input-background-color: var(--sl-color-neutral-0);\n --sl-input-background-color-hover: var(--sl-input-background-color);\n --sl-input-background-color-focus: var(--sl-input-background-color);\n --sl-input-background-color-disabled: var(--sl-color-neutral-100);\n --sl-input-border-color: var(--sl-color-neutral-300);\n --sl-input-border-color-hover: var(--sl-color-neutral-400);\n --sl-input-border-color-focus: var(--sl-color-primary-500);\n --sl-input-border-color-disabled: var(--sl-color-neutral-300);\n --sl-input-border-width: 1px;\n --sl-input-required-content: \"*\";\n --sl-input-required-content-offset: -2px;\n --sl-input-required-content-color: var(--sl-input-label-color);\n\n --sl-input-border-radius-small: var(--sl-border-radius-medium);\n --sl-input-border-radius-medium: var(--sl-border-radius-medium);\n --sl-input-border-radius-large: var(--sl-border-radius-medium);\n\n --sl-input-font-family: var(--sl-font-sans);\n --sl-input-font-weight: var(--sl-font-weight-normal);\n --sl-input-font-size-small: var(--sl-font-size-small);\n --sl-input-font-size-medium: var(--sl-font-size-medium);\n --sl-input-font-size-large: var(--sl-font-size-large);\n --sl-input-letter-spacing: var(--sl-letter-spacing-normal);\n\n --sl-input-color: var(--sl-color-neutral-700);\n --sl-input-color-hover: var(--sl-color-neutral-700);\n --sl-input-color-focus: var(--sl-color-neutral-700);\n --sl-input-color-disabled: var(--sl-color-neutral-900);\n --sl-input-icon-color: var(--sl-color-neutral-500);\n --sl-input-icon-color-hover: var(--sl-color-neutral-600);\n --sl-input-icon-color-focus: var(--sl-color-neutral-600);\n --sl-input-placeholder-color: var(--sl-color-neutral-500);\n --sl-input-placeholder-color-disabled: var(--sl-color-neutral-600);\n --sl-input-spacing-small: var(--sl-spacing-small);\n --sl-input-spacing-medium: var(--sl-spacing-medium);\n --sl-input-spacing-large: var(--sl-spacing-large);\n\n --sl-input-focus-ring-color: hsl(198.6 88.7% 48.4% / 40%);\n --sl-input-focus-ring-offset: 0;\n\n --sl-input-filled-background-color: var(--sl-color-neutral-100);\n --sl-input-filled-background-color-hover: var(--sl-color-neutral-100);\n --sl-input-filled-background-color-focus: var(--sl-color-neutral-100);\n --sl-input-filled-background-color-disabled: var(--sl-color-neutral-100);\n --sl-input-filled-color: var(--sl-color-neutral-800);\n --sl-input-filled-color-hover: var(--sl-color-neutral-800);\n --sl-input-filled-color-focus: var(--sl-color-neutral-700);\n --sl-input-filled-color-disabled: var(--sl-color-neutral-800);\n\n --sl-input-label-font-size-small: var(--sl-font-size-small);\n --sl-input-label-font-size-medium: var(--sl-font-size-medium);\n --sl-input-label-font-size-large: var(--sl-font-size-large);\n --sl-input-label-color: inherit;\n\n --sl-input-help-text-font-size-small: var(--sl-font-size-x-small);\n --sl-input-help-text-font-size-medium: var(--sl-font-size-small);\n --sl-input-help-text-font-size-large: var(--sl-font-size-medium);\n --sl-input-help-text-color: var(--sl-color-neutral-500);\n\n --sl-toggle-size-small: 0.875rem;\n --sl-toggle-size-medium: 1.125rem;\n --sl-toggle-size-large: 1.375rem;\n\n --sl-overlay-background-color: hsl(240 3.8% 46.1% / 33%);\n\n --sl-panel-background-color: var(--sl-color-neutral-0);\n --sl-panel-border-color: var(--sl-color-neutral-200);\n --sl-panel-border-width: 1px;\n\n --sl-tooltip-border-radius: var(--sl-border-radius-medium);\n --sl-tooltip-background-color: var(--sl-color-neutral-800);\n --sl-tooltip-color: var(--sl-color-neutral-0);\n --sl-tooltip-font-family: var(--sl-font-sans);\n --sl-tooltip-font-weight: var(--sl-font-weight-normal);\n --sl-tooltip-font-size: var(--sl-font-size-small);\n --sl-tooltip-line-height: var(--sl-line-height-dense);\n --sl-tooltip-padding: var(--sl-spacing-2x-small) var(--sl-spacing-x-small);\n --sl-tooltip-arrow-size: 6px;\n\n --sl-z-index-drawer: 999700;\n --sl-z-index-dialog: 999800;\n --sl-z-index-dropdown: 999900;\n --sl-z-index-toast: 999950;\n --sl-z-index-tooltip: 9991000;\n}\n\n.sl-scroll-lock {\n padding-right: var(--sl-scroll-lock-size) !important;\n overflow: hidden !important;\n}\n\n.sl-toast-stack {\n position: fixed;\n top: 0;\n inset-inline-end: 0;\n z-index: var(--sl-z-index-toast);\n width: 28rem;\n max-width: 100%;\n max-height: 100%;\n overflow: auto;\n}\n\n.sl-toast-stack sl-alert {\n margin: var(--sl-spacing-medium);\n}\n\n.sl-toast-stack sl-alert::part(base) {\n box-shadow: var(--sl-shadow-large);\n}\n\nsl-drawer::part(base) {\n color: var(--sl-color-neutral-800) !important;\n}\n\n.h5player-popup-wrap {\n position: relative;\n z-index: 99999999;\n opacity: 0;\n}\n\n.h5player-popup-wrap sl-popup {\n position: relative;\n}\n\n.h5player-popup-wrap .h5player-popup-content {\n background-color: rgba(0, 0, 0, 0.9);\n color: #fff;\n font-size: 16px;\n min-width: 220px;\n height: 48px;\n line-height: 48px;\n display: flex;\n padding: 0 16px;\n border-radius: 6px 6px 0 0;\n border-bottom: 2px solid rgba(255, 255, 255, 0.2);\n\n /* 灰色向下的过度阴影 */\n box-shadow: 0 6px 14px rgba(0, 0, 0, 0.7);\n\n display: flex;\n align-items: center;\n justify-content: space-between;\n}\n\n@keyframes text-lumos {\n 0%,100%{ color:#fff; }\n\t50%{ color:#ccc; }\n}\n\n.h5player-popup-content .h5p-logo-mod {\n white-space: nowrap;\n font-weight: 500;\n text-shadow: 0px 0px 2px #666, 0 0 30px #666;\n animation: text-lumos 5s infinite;\n}\n\n.h5player-popup-content .h5p-menu-wrap {}\n\n.h5player-popup-content .h5p-action-mod {\n display: flex;\n align-items: center;\n justify-content: space-between;\n}\n\n.h5player-popup-content .h5p-action-btn {\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 14px;\n padding: 0 8px;\n cursor: pointer;\n white-space: nowrap;\n}\n\n.h5player-popup-content .h5p-action-btn:hover {\n background-color: rgba(255, 255, 255, 0.2);\n}\n\n.h5player-popup-content .h5p-action-btn sl-icon {\n padding: 0 4px;\n}\n\n/* 激活态 */\n.h5player-popup-active {\n opacity: 0.8;\n transition: opacity 0.2s;\n}\n\n.h5player-popup-content a, .h5player-popup-content a:visited{\n color: #fff;\n cursor: pointer;\n text-decoration: none;\n}\n\n.h5player-popup-wrap:hover, .h5player-popup-full-active {\n opacity: 1 !important;\n transition: opacity 0.2s;\n}\n\n.h5player-popup-wrap:hover .h5player-popup-content, .h5player-popup-full-active .h5player-popup-content {\n border-bottom: 2px solid rgba(255, 255, 255, 0.6);\n}\n\n.h5player-popup-content .h5p-action-mod sl-menu {\n background-color: rgba(0, 0, 0, 0.9);\n color: #fff;\n border-radius: 4px;\n padding: 5px 0;\n}\n\n.h5player-popup-content .h5p-action-mod sl-menu-item::part(base) {\n /* background-color: rgba(0, 0, 0, 0.9); */\n color: #fff;\n font-size: 14px;\n padding: 2px 0;\n}\n\n.h5player-popup-content .h5p-action-mod sl-menu-item::part(base):hover {\n background-color: var(--sl-color-primary-500);\n color: #fff;\n}\n\n.h5player-popup-content .h5p-recommend-wrap {\n flex-grow: 1;\n box-sizing: border-box;\n margin: 0 20px;\n text-align: center;\n font-size: 14px;\n overflow: hidden;\n white-space: nowrap;\n\n display: flex;\n justify-content: flex-end;\n align-items: center;\n position: relative;\n}\n\n@keyframes text-marquee {\n 0% { transform: translateX(0); }\n 100% { transform: translateX(-100%); }\n}\n\n.h5player-popup-content .h5p-recommend-mod {\n display: inline-block;\n word-break: keep-all;\n white-space: nowrap;\n /* 无限循环滚动的动画效果 */\n /* padding-left: 100%; */\n /* animation: text-marquee 15s linear infinite; */\n}\n.h5player-popup-content .h5p-recommend-item {\n word-break: keep-all;\n white-space: nowrap;\n\n position: absolute;\n top: 0;\n right: 0;\n pointer-events: none;\n opacity: 0;\n transition: opacity 0.4s;\n}\n\n.h5player-popup-content .h5p-recommend-item__active {\n opacity: 1;\n z-index: 99;\n pointer-events: auto;\n}\n\n.h5player-popup-content .h5p-recommend-wrap>div {\n opacity: 0.5;\n}\n.h5player-popup-content .h5p-recommend-wrap>div:hover{\n opacity: 1;\n}\n.h5player-popup-content .h5p-recommend-wrap>div:hover .h5p-recommend-mod {\n animation-play-state: paused;\n}"); /** * @license * Copyright 2019 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ const t$2=globalThis,e$8=t$2.ShadowRoot&&(void 0===t$2.ShadyCSS||t$2.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,s$3=Symbol(),o$5=new WeakMap;let n$5 = class n{constructor(t,e,o){if(this._$cssResult$=!0,o!==s$3)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=t,this.t=e;}get styleSheet(){let t=this.o;const s=this.t;if(e$8&&void 0===t){const e=void 0!==s&&1===s.length;e&&(t=o$5.get(s)),void 0===t&&((this.o=t=new CSSStyleSheet).replaceSync(this.cssText),e&&o$5.set(s,t));}return t}toString(){return this.cssText}};const r$6=t=>new n$5("string"==typeof t?t:t+"",void 0,s$3),i$3=(t,...e)=>{const o=1===t.length?t[0]:e.reduce(((e,s,o)=>e+(t=>{if(!0===t._$cssResult$)return t.cssText;if("number"==typeof t)return t;throw Error("Value passed to 'css' function must be a 'css' function result: "+t+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(s)+t[o+1]),t[0]);return new n$5(o,t,s$3)},S$1=(s,o)=>{if(e$8)s.adoptedStyleSheets=o.map((t=>t instanceof CSSStyleSheet?t:t.styleSheet));else for(const e of o){const o=document.createElement("style"),n=t$2.litNonce;void 0!==n&&o.setAttribute("nonce",n),o.textContent=e.cssText,s.appendChild(o);}},c$3=e$8?t=>t:t=>t instanceof CSSStyleSheet?(t=>{let e="";for(const s of t.cssRules)e+=s.cssText;return r$6(e)})(t):t; /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */const{is:i$2,defineProperty:e$7,getOwnPropertyDescriptor:r$5,getOwnPropertyNames:h$3,getOwnPropertySymbols:o$4,getPrototypeOf:n$4}=Object,a$1=globalThis,c$2=a$1.trustedTypes,l$1=c$2?c$2.emptyScript:"",p$1=a$1.reactiveElementPolyfillSupport,d$1=(t,s)=>t,u$1={toAttribute(t,s){switch(s){case Boolean:t=t?l$1:null;break;case Object:case Array:t=null==t?t:JSON.stringify(t);}return t},fromAttribute(t,s){let i=t;switch(s){case Boolean:i=null!==t;break;case Number:i=null===t?null:Number(t);break;case Object:case Array:try{i=JSON.parse(t);}catch(t){i=null;}}return i}},f$3=(t,s)=>!i$2(t,s),y$1={attribute:!0,type:String,converter:u$1,reflect:!1,hasChanged:f$3};Symbol.metadata??=Symbol("metadata"),a$1.litPropertyMetadata??=new WeakMap;class b extends HTMLElement{static addInitializer(t){this._$Ei(),(this.l??=[]).push(t);}static get observedAttributes(){return this.finalize(),this._$Eh&&[...this._$Eh.keys()]}static createProperty(t,s=y$1){if(s.state&&(s.attribute=!1),this._$Ei(),this.elementProperties.set(t,s),!s.noAccessor){const i=Symbol(),r=this.getPropertyDescriptor(t,i,s);void 0!==r&&e$7(this.prototype,t,r);}}static getPropertyDescriptor(t,s,i){const{get:e,set:h}=r$5(this.prototype,t)??{get(){return this[s]},set(t){this[s]=t;}};return {get(){return e?.call(this)},set(s){const r=e?.call(this);h.call(this,s),this.requestUpdate(t,r,i);},configurable:!0,enumerable:!0}}static getPropertyOptions(t){return this.elementProperties.get(t)??y$1}static _$Ei(){if(this.hasOwnProperty(d$1("elementProperties")))return;const t=n$4(this);t.finalize(),void 0!==t.l&&(this.l=[...t.l]),this.elementProperties=new Map(t.elementProperties);}static finalize(){if(this.hasOwnProperty(d$1("finalized")))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(d$1("properties"))){const t=this.properties,s=[...h$3(t),...o$4(t)];for(const i of s)this.createProperty(i,t[i]);}const t=this[Symbol.metadata];if(null!==t){const s=litPropertyMetadata.get(t);if(void 0!==s)for(const[t,i]of s)this.elementProperties.set(t,i);}this._$Eh=new Map;for(const[t,s]of this.elementProperties){const i=this._$Eu(t,s);void 0!==i&&this._$Eh.set(i,t);}this.elementStyles=this.finalizeStyles(this.styles);}static finalizeStyles(s){const i=[];if(Array.isArray(s)){const e=new Set(s.flat(1/0).reverse());for(const s of e)i.unshift(c$3(s));}else void 0!==s&&i.push(c$3(s));return i}static _$Eu(t,s){const i=s.attribute;return !1===i?void 0:"string"==typeof i?i:"string"==typeof t?t.toLowerCase():void 0}constructor(){super(),this._$Ep=void 0,this.isUpdatePending=!1,this.hasUpdated=!1,this._$Em=null,this._$Ev();}_$Ev(){this._$Eg=new Promise((t=>this.enableUpdating=t)),this._$AL=new Map,this._$ES(),this.requestUpdate(),this.constructor.l?.forEach((t=>t(this)));}addController(t){(this._$E_??=new Set).add(t),void 0!==this.renderRoot&&this.isConnected&&t.hostConnected?.();}removeController(t){this._$E_?.delete(t);}_$ES(){const t=new Map,s=this.constructor.elementProperties;for(const i of s.keys())this.hasOwnProperty(i)&&(t.set(i,this[i]),delete this[i]);t.size>0&&(this._$Ep=t);}createRenderRoot(){const t=this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions);return S$1(t,this.constructor.elementStyles),t}connectedCallback(){this.renderRoot??=this.createRenderRoot(),this.enableUpdating(!0),this._$E_?.forEach((t=>t.hostConnected?.()));}enableUpdating(t){}disconnectedCallback(){this._$E_?.forEach((t=>t.hostDisconnected?.()));}attributeChangedCallback(t,s,i){this._$AK(t,i);}_$EO(t,s){const i=this.constructor.elementProperties.get(t),e=this.constructor._$Eu(t,i);if(void 0!==e&&!0===i.reflect){const r=(void 0!==i.converter?.toAttribute?i.converter:u$1).toAttribute(s,i.type);this._$Em=t,null==r?this.removeAttribute(e):this.setAttribute(e,r),this._$Em=null;}}_$AK(t,s){const i=this.constructor,e=i._$Eh.get(t);if(void 0!==e&&this._$Em!==e){const t=i.getPropertyOptions(e),r="function"==typeof t.converter?{fromAttribute:t.converter}:void 0!==t.converter?.fromAttribute?t.converter:u$1;this._$Em=e,this[e]=r.fromAttribute(s,t.type),this._$Em=null;}}requestUpdate(t,s,i){if(void 0!==t){if(i??=this.constructor.getPropertyOptions(t),!(i.hasChanged??f$3)(this[t],s))return;this.C(t,s,i);}!1===this.isUpdatePending&&(this._$Eg=this._$EP());}C(t,s,i){this._$AL.has(t)||this._$AL.set(t,s),!0===i.reflect&&this._$Em!==t&&(this._$ET??=new Set).add(t);}async _$EP(){this.isUpdatePending=!0;try{await this._$Eg;}catch(t){Promise.reject(t);}const t=this.scheduleUpdate();return null!=t&&await t,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this.renderRoot??=this.createRenderRoot(),this._$Ep){for(const[t,s]of this._$Ep)this[t]=s;this._$Ep=void 0;}const t=this.constructor.elementProperties;if(t.size>0)for(const[s,i]of t)!0!==i.wrapped||this._$AL.has(s)||void 0===this[s]||this.C(s,this[s],i);}let t=!1;const s=this._$AL;try{t=this.shouldUpdate(s),t?(this.willUpdate(s),this._$E_?.forEach((t=>t.hostUpdate?.())),this.update(s)):this._$Ej();}catch(s){throw t=!1,this._$Ej(),s}t&&this._$AE(s);}willUpdate(t){}_$AE(t){this._$E_?.forEach((t=>t.hostUpdated?.())),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(t)),this.updated(t);}_$Ej(){this._$AL=new Map,this.isUpdatePending=!1;}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$Eg}shouldUpdate(t){return !0}update(t){this._$ET&&=this._$ET.forEach((t=>this._$EO(t,this[t]))),this._$Ej();}updated(t){}firstUpdated(t){}}b.elementStyles=[],b.shadowRootOptions={mode:"open"},b[d$1("elementProperties")]=new Map,b[d$1("finalized")]=new Map,p$1?.({ReactiveElement:b}),(a$1.reactiveElementVersions??=[]).push("2.0.3"); /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ const t$1=globalThis,i$1=t$1.trustedTypes,s$2=i$1?i$1.createPolicy("lit-html",{createHTML:t=>t}):void 0,e$6="$lit$",h$2=`lit$${(Math.random()+"").slice(9)}$`,o$3="?"+h$2,n$3=`<${o$3}>`,r$4=document,l=()=>r$4.createComment(""),c$1=t=>null===t||"object"!=typeof t&&"function"!=typeof t,a=Array.isArray,u=t=>a(t)||"function"==typeof t?.[Symbol.iterator],d="[ \t\n\f\r]",f$2=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,v=/-->/g,_=/>/g,m=RegExp(`>|${d}(?:([^\\s"'>=/]+)(${d}*=${d}*(?:[^ \t\n\f\r"'\`<>=]|("|')|))|$)`,"g"),p=/'/g,g=/"/g,$=/^(?:script|style|textarea|title)$/i,y=t=>(i,...s)=>({_$litType$:t,strings:i,values:s}),x=y(1),w=Symbol.for("lit-noChange"),T=Symbol.for("lit-nothing"),A=new WeakMap,E=r$4.createTreeWalker(r$4,129);function C(t,i){if(!Array.isArray(t)||!t.hasOwnProperty("raw"))throw Error("invalid template strings array");return void 0!==s$2?s$2.createHTML(i):i}const P=(t,i)=>{const s=t.length-1,o=[];let r,l=2===i?"":"",c=f$2;for(let i=0;i"===u[0]?(c=r??f$2,d=-1):void 0===u[1]?d=-2:(d=c.lastIndex-u[2].length,a=u[1],c=void 0===u[3]?m:'"'===u[3]?g:p):c===g||c===p?c=m:c===v||c===_?c=f$2:(c=m,r=void 0);const x=c===m&&t[i+1].startsWith("/>")?" ":"";l+=c===f$2?s+n$3:d>=0?(o.push(a),s.slice(0,d)+e$6+s.slice(d)+h$2+x):s+h$2+(-2===d?i:x);}return [C(t,l+(t[s]||"")+(2===i?"":"")),o]};class V{constructor({strings:t,_$litType$:s},n){let r;this.parts=[];let c=0,a=0;const u=t.length-1,d=this.parts,[f,v]=P(t,s);if(this.el=V.createElement(f,n),E.currentNode=this.el.content,2===s){const t=this.el.content.firstChild;t.replaceWith(...t.childNodes);}for(;null!==(r=E.nextNode())&&d.length0){r.textContent=i$1?i$1.emptyScript:"";for(let i=0;i2||""!==s[0]||""!==s[1]?(this._$AH=Array(s.length-1).fill(new String),this.strings=s):this._$AH=T;}_$AI(t,i=this,s,e){const h=this.strings;let o=!1;if(void 0===h)t=N(this,t,i,0),o=!c$1(t)||t!==this._$AH&&t!==w,o&&(this._$AH=t);else {const e=t;let n,r;for(t=h[0],n=0;n{const e=s?.renderBefore??i;let h=e._$litPart$;if(void 0===h){const t=s?.renderBefore??null;e._$litPart$=h=new M(i.insertBefore(l(),t),t,void 0,s??{});}return h._$AI(t),h}; /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */let s$1 = class s extends b{constructor(){super(...arguments),this.renderOptions={host:this},this._$Do=void 0;}createRenderRoot(){const t=super.createRenderRoot();return this.renderOptions.renderBefore??=t.firstChild,t}update(t){const i=this.render();this.hasUpdated||(this.renderOptions.isConnected=this.isConnected),super.update(t),this._$Do=j(i,this.renderRoot,this.renderOptions);}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(!0);}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(!1);}render(){return w}};s$1._$litElement$=!0,s$1[("finalized")]=!0,globalThis.litElementHydrateSupport?.({LitElement:s$1});const r$3=globalThis.litElementPolyfillSupport;r$3?.({LitElement:s$1});(globalThis.litElementVersions??=[]).push("4.0.3"); // src/styles/component.styles.ts var component_styles_default = i$3` :host { box-sizing: border-box; } :host *, :host *::before, :host *::after { box-sizing: inherit; } [hidden] { display: none !important; } `; var popup_styles_default = i$3` ${component_styles_default} :host { --arrow-color: var(--sl-color-neutral-1000); --arrow-size: 6px; /* * These properties are computed to account for the arrow's dimensions after being rotated 45º. The constant * 0.7071 is derived from sin(45), which is the diagonal size of the arrow's container after rotating. */ --arrow-size-diagonal: calc(var(--arrow-size) * 0.7071); --arrow-padding-offset: calc(var(--arrow-size-diagonal) - var(--arrow-size)); display: contents; } .popup { position: absolute; isolation: isolate; max-width: var(--auto-size-available-width, none); max-height: var(--auto-size-available-height, none); } .popup--fixed { position: fixed; } .popup:not(.popup--active) { display: none; } .popup__arrow { position: absolute; width: calc(var(--arrow-size-diagonal) * 2); height: calc(var(--arrow-size-diagonal) * 2); rotate: 45deg; background: var(--arrow-color); z-index: -1; } `; var __defProp = Object.defineProperty; var __defProps = Object.defineProperties; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropDescs = Object.getOwnPropertyDescriptors; var __getOwnPropSymbols = Object.getOwnPropertySymbols; var __hasOwnProp = Object.prototype.hasOwnProperty; var __propIsEnum = Object.prototype.propertyIsEnumerable; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __spreadValues = (a, b) => { for (var prop in b || (b = {})) if (__hasOwnProp.call(b, prop)) __defNormalProp(a, prop, b[prop]); if (__getOwnPropSymbols) for (var prop of __getOwnPropSymbols(b)) { if (__propIsEnum.call(b, prop)) __defNormalProp(a, prop, b[prop]); } return a; }; var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b)); var __decorateClass = (decorators, target, key, kind) => { var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target; for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result; if (kind && result) __defProp(target, key, result); return result; }; /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */const o$2={attribute:!0,type:String,converter:u$1,reflect:!1,hasChanged:f$3},r$2=(t=o$2,e,r)=>{const{kind:n,metadata:i}=r;let s=globalThis.litPropertyMetadata.get(i);if(void 0===s&&globalThis.litPropertyMetadata.set(i,s=new Map),s.set(r.name,t),"accessor"===n){const{name:o}=r;return {set(r){const n=e.get.call(this);e.set.call(this,r),this.requestUpdate(o,n,t);},init(e){return void 0!==e&&this.C(o,void 0,t),e}}}if("setter"===n){const{name:o}=r;return function(r){const n=this[o];e.call(this,r),this.requestUpdate(o,n,t);}}throw Error("Unsupported decorator location: "+n)};function n$2(t){return (e,o)=>"object"==typeof o?r$2(t,e,o):((t,e,o)=>{const r=e.hasOwnProperty(o);return e.constructor.createProperty(o,r?{...t,wrapped:!0}:t),r?Object.getOwnPropertyDescriptor(e,o):void 0})(t,e,o)} /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */function r$1(r){return n$2({...r,state:!0,attribute:!1})} /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ const e$5=(e,t,c)=>(c.configurable=!0,c.enumerable=!0,Reflect.decorate&&"object"!=typeof t&&Object.defineProperty(e,t,c),c); /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */function e$4(e,r){return (n,s,i)=>{const o=t=>t.renderRoot?.querySelector(e)??null;if(r){const{get:e,set:r}="object"==typeof s?n:i??(()=>{const t=Symbol();return {get(){return this[t]},set(e){this[t]=e;}}})();return e$5(n,s,{get(){let t=e.call(this);return void 0===t&&(t=o(this),(null!==t||this.hasUpdated)&&r.call(this,t)),t}})}return e$5(n,s,{get(){return o(this)}})}} var ShoelaceElement = class extends s$1 { constructor() { super(); Object.entries(this.constructor.dependencies).forEach(([name, component]) => { this.constructor.define(name, component); }); } emit(name, options) { const event = new CustomEvent(name, __spreadValues({ bubbles: true, cancelable: false, composed: true, detail: {} }, options)); this.dispatchEvent(event); return event; } /* eslint-enable */ static define(name, elementConstructor = this, options = {}) { const currentlyRegisteredConstructor = customElements.get(name); if (!currentlyRegisteredConstructor) { customElements.define(name, class extends elementConstructor { }, options); return; } let newVersion = " (unknown version)"; let existingVersion = newVersion; if ("version" in elementConstructor && elementConstructor.version) { newVersion = " v" + elementConstructor.version; } if ("version" in currentlyRegisteredConstructor && currentlyRegisteredConstructor.version) { existingVersion = " v" + currentlyRegisteredConstructor.version; } if (newVersion && existingVersion && newVersion === existingVersion) { return; } console.warn( `Attempted to register <${name}>${newVersion}, but <${name}>${existingVersion} has already been registered.` ); } }; /* eslint-disable */ // @ts-expect-error This is auto-injected at build time. ShoelaceElement.version = "2.12.0"; ShoelaceElement.dependencies = {}; __decorateClass([ n$2() ], ShoelaceElement.prototype, "dir", 2); __decorateClass([ n$2() ], ShoelaceElement.prototype, "lang", 2); /** * Custom positioning reference element. * @see https://floating-ui.com/docs/virtual-elements */ const min = Math.min; const max = Math.max; const round = Math.round; const floor = Math.floor; const createCoords = v => ({ x: v, y: v }); const oppositeSideMap = { left: 'right', right: 'left', bottom: 'top', top: 'bottom' }; const oppositeAlignmentMap = { start: 'end', end: 'start' }; function clamp(start, value, end) { return max(start, min(value, end)); } function evaluate(value, param) { return typeof value === 'function' ? value(param) : value; } function getSide(placement) { return placement.split('-')[0]; } function getAlignment(placement) { return placement.split('-')[1]; } function getOppositeAxis(axis) { return axis === 'x' ? 'y' : 'x'; } function getAxisLength(axis) { return axis === 'y' ? 'height' : 'width'; } function getSideAxis(placement) { return ['top', 'bottom'].includes(getSide(placement)) ? 'y' : 'x'; } function getAlignmentAxis(placement) { return getOppositeAxis(getSideAxis(placement)); } function getAlignmentSides(placement, rects, rtl) { if (rtl === void 0) { rtl = false; } const alignment = getAlignment(placement); const alignmentAxis = getAlignmentAxis(placement); const length = getAxisLength(alignmentAxis); let mainAlignmentSide = alignmentAxis === 'x' ? alignment === (rtl ? 'end' : 'start') ? 'right' : 'left' : alignment === 'start' ? 'bottom' : 'top'; if (rects.reference[length] > rects.floating[length]) { mainAlignmentSide = getOppositePlacement(mainAlignmentSide); } return [mainAlignmentSide, getOppositePlacement(mainAlignmentSide)]; } function getExpandedPlacements(placement) { const oppositePlacement = getOppositePlacement(placement); return [getOppositeAlignmentPlacement(placement), oppositePlacement, getOppositeAlignmentPlacement(oppositePlacement)]; } function getOppositeAlignmentPlacement(placement) { return placement.replace(/start|end/g, alignment => oppositeAlignmentMap[alignment]); } function getSideList(side, isStart, rtl) { const lr = ['left', 'right']; const rl = ['right', 'left']; const tb = ['top', 'bottom']; const bt = ['bottom', 'top']; switch (side) { case 'top': case 'bottom': if (rtl) return isStart ? rl : lr; return isStart ? lr : rl; case 'left': case 'right': return isStart ? tb : bt; default: return []; } } function getOppositeAxisPlacements(placement, flipAlignment, direction, rtl) { const alignment = getAlignment(placement); let list = getSideList(getSide(placement), direction === 'start', rtl); if (alignment) { list = list.map(side => side + "-" + alignment); if (flipAlignment) { list = list.concat(list.map(getOppositeAlignmentPlacement)); } } return list; } function getOppositePlacement(placement) { return placement.replace(/left|right|bottom|top/g, side => oppositeSideMap[side]); } function expandPaddingObject(padding) { return { top: 0, right: 0, bottom: 0, left: 0, ...padding }; } function getPaddingObject(padding) { return typeof padding !== 'number' ? expandPaddingObject(padding) : { top: padding, right: padding, bottom: padding, left: padding }; } function rectToClientRect(rect) { return { ...rect, top: rect.y, left: rect.x, right: rect.x + rect.width, bottom: rect.y + rect.height }; } function computeCoordsFromPlacement(_ref, placement, rtl) { let { reference, floating } = _ref; const sideAxis = getSideAxis(placement); const alignmentAxis = getAlignmentAxis(placement); const alignLength = getAxisLength(alignmentAxis); const side = getSide(placement); const isVertical = sideAxis === 'y'; const commonX = reference.x + reference.width / 2 - floating.width / 2; const commonY = reference.y + reference.height / 2 - floating.height / 2; const commonAlign = reference[alignLength] / 2 - floating[alignLength] / 2; let coords; switch (side) { case 'top': coords = { x: commonX, y: reference.y - floating.height }; break; case 'bottom': coords = { x: commonX, y: reference.y + reference.height }; break; case 'right': coords = { x: reference.x + reference.width, y: commonY }; break; case 'left': coords = { x: reference.x - floating.width, y: commonY }; break; default: coords = { x: reference.x, y: reference.y }; } switch (getAlignment(placement)) { case 'start': coords[alignmentAxis] -= commonAlign * (rtl && isVertical ? -1 : 1); break; case 'end': coords[alignmentAxis] += commonAlign * (rtl && isVertical ? -1 : 1); break; } return coords; } /** * Computes the `x` and `y` coordinates that will place the floating element * next to a given reference element. * * This export does not have any `platform` interface logic. You will need to * write one for the platform you are using Floating UI with. */ const computePosition$1 = async (reference, floating, config) => { const { placement = 'bottom', strategy = 'absolute', middleware = [], platform } = config; const validMiddleware = middleware.filter(Boolean); const rtl = await (platform.isRTL == null ? void 0 : platform.isRTL(floating)); let rects = await platform.getElementRects({ reference, floating, strategy }); let { x, y } = computeCoordsFromPlacement(rects, placement, rtl); let statefulPlacement = placement; let middlewareData = {}; let resetCount = 0; for (let i = 0; i < validMiddleware.length; i++) { const { name, fn } = validMiddleware[i]; const { x: nextX, y: nextY, data, reset } = await fn({ x, y, initialPlacement: placement, placement: statefulPlacement, strategy, middlewareData, rects, platform, elements: { reference, floating } }); x = nextX != null ? nextX : x; y = nextY != null ? nextY : y; middlewareData = { ...middlewareData, [name]: { ...middlewareData[name], ...data } }; if (reset && resetCount <= 50) { resetCount++; if (typeof reset === 'object') { if (reset.placement) { statefulPlacement = reset.placement; } if (reset.rects) { rects = reset.rects === true ? await platform.getElementRects({ reference, floating, strategy }) : reset.rects; } ({ x, y } = computeCoordsFromPlacement(rects, statefulPlacement, rtl)); } i = -1; continue; } } return { x, y, placement: statefulPlacement, strategy, middlewareData }; }; /** * Resolves with an object of overflow side offsets that determine how much the * element is overflowing a given clipping boundary on each side. * - positive = overflowing the boundary by that number of pixels * - negative = how many pixels left before it will overflow * - 0 = lies flush with the boundary * @see https://floating-ui.com/docs/detectOverflow */ async function detectOverflow(state, options) { var _await$platform$isEle; if (options === void 0) { options = {}; } const { x, y, platform, rects, elements, strategy } = state; const { boundary = 'clippingAncestors', rootBoundary = 'viewport', elementContext = 'floating', altBoundary = false, padding = 0 } = evaluate(options, state); const paddingObject = getPaddingObject(padding); const altContext = elementContext === 'floating' ? 'reference' : 'floating'; const element = elements[altBoundary ? altContext : elementContext]; const clippingClientRect = rectToClientRect(await platform.getClippingRect({ element: ((_await$platform$isEle = await (platform.isElement == null ? void 0 : platform.isElement(element))) != null ? _await$platform$isEle : true) ? element : element.contextElement || (await (platform.getDocumentElement == null ? void 0 : platform.getDocumentElement(elements.floating))), boundary, rootBoundary, strategy })); const rect = elementContext === 'floating' ? { ...rects.floating, x, y } : rects.reference; const offsetParent = await (platform.getOffsetParent == null ? void 0 : platform.getOffsetParent(elements.floating)); const offsetScale = (await (platform.isElement == null ? void 0 : platform.isElement(offsetParent))) ? (await (platform.getScale == null ? void 0 : platform.getScale(offsetParent))) || { x: 1, y: 1 } : { x: 1, y: 1 }; const elementClientRect = rectToClientRect(platform.convertOffsetParentRelativeRectToViewportRelativeRect ? await platform.convertOffsetParentRelativeRectToViewportRelativeRect({ rect, offsetParent, strategy }) : rect); return { top: (clippingClientRect.top - elementClientRect.top + paddingObject.top) / offsetScale.y, bottom: (elementClientRect.bottom - clippingClientRect.bottom + paddingObject.bottom) / offsetScale.y, left: (clippingClientRect.left - elementClientRect.left + paddingObject.left) / offsetScale.x, right: (elementClientRect.right - clippingClientRect.right + paddingObject.right) / offsetScale.x }; } /** * Provides data to position an inner element of the floating element so that it * appears centered to the reference element. * @see https://floating-ui.com/docs/arrow */ const arrow$1 = options => ({ name: 'arrow', options, async fn(state) { const { x, y, placement, rects, platform, elements, middlewareData } = state; // Since `element` is required, we don't Partial<> the type. const { element, padding = 0 } = evaluate(options, state) || {}; if (element == null) { return {}; } const paddingObject = getPaddingObject(padding); const coords = { x, y }; const axis = getAlignmentAxis(placement); const length = getAxisLength(axis); const arrowDimensions = await platform.getDimensions(element); const isYAxis = axis === 'y'; const minProp = isYAxis ? 'top' : 'left'; const maxProp = isYAxis ? 'bottom' : 'right'; const clientProp = isYAxis ? 'clientHeight' : 'clientWidth'; const endDiff = rects.reference[length] + rects.reference[axis] - coords[axis] - rects.floating[length]; const startDiff = coords[axis] - rects.reference[axis]; const arrowOffsetParent = await (platform.getOffsetParent == null ? void 0 : platform.getOffsetParent(element)); let clientSize = arrowOffsetParent ? arrowOffsetParent[clientProp] : 0; // DOM platform can return `window` as the `offsetParent`. if (!clientSize || !(await (platform.isElement == null ? void 0 : platform.isElement(arrowOffsetParent)))) { clientSize = elements.floating[clientProp] || rects.floating[length]; } const centerToReference = endDiff / 2 - startDiff / 2; // If the padding is large enough that it causes the arrow to no longer be // centered, modify the padding so that it is centered. const largestPossiblePadding = clientSize / 2 - arrowDimensions[length] / 2 - 1; const minPadding = min(paddingObject[minProp], largestPossiblePadding); const maxPadding = min(paddingObject[maxProp], largestPossiblePadding); // Make sure the arrow doesn't overflow the floating element if the center // point is outside the floating element's bounds. const min$1 = minPadding; const max = clientSize - arrowDimensions[length] - maxPadding; const center = clientSize / 2 - arrowDimensions[length] / 2 + centerToReference; const offset = clamp(min$1, center, max); // If the reference is small enough that the arrow's padding causes it to // to point to nothing for an aligned placement, adjust the offset of the // floating element itself. To ensure `shift()` continues to take action, // a single reset is performed when this is true. const shouldAddOffset = !middlewareData.arrow && getAlignment(placement) != null && center != offset && rects.reference[length] / 2 - (center < min$1 ? minPadding : maxPadding) - arrowDimensions[length] / 2 < 0; const alignmentOffset = shouldAddOffset ? center < min$1 ? center - min$1 : center - max : 0; return { [axis]: coords[axis] + alignmentOffset, data: { [axis]: offset, centerOffset: center - offset - alignmentOffset, ...(shouldAddOffset && { alignmentOffset }) }, reset: shouldAddOffset }; } }); /** * Optimizes the visibility of the floating element by flipping the `placement` * in order to keep it in view when the preferred placement(s) will overflow the * clipping boundary. Alternative to `autoPlacement`. * @see https://floating-ui.com/docs/flip */ const flip$1 = function (options) { if (options === void 0) { options = {}; } return { name: 'flip', options, async fn(state) { var _middlewareData$arrow, _middlewareData$flip; const { placement, middlewareData, rects, initialPlacement, platform, elements } = state; const { mainAxis: checkMainAxis = true, crossAxis: checkCrossAxis = true, fallbackPlacements: specifiedFallbackPlacements, fallbackStrategy = 'bestFit', fallbackAxisSideDirection = 'none', flipAlignment = true, ...detectOverflowOptions } = evaluate(options, state); // If a reset by the arrow was caused due to an alignment offset being // added, we should skip any logic now since `flip()` has already done its // work. // https://github.com/floating-ui/floating-ui/issues/2549#issuecomment-1719601643 if ((_middlewareData$arrow = middlewareData.arrow) != null && _middlewareData$arrow.alignmentOffset) { return {}; } const side = getSide(placement); const isBasePlacement = getSide(initialPlacement) === initialPlacement; const rtl = await (platform.isRTL == null ? void 0 : platform.isRTL(elements.floating)); const fallbackPlacements = specifiedFallbackPlacements || (isBasePlacement || !flipAlignment ? [getOppositePlacement(initialPlacement)] : getExpandedPlacements(initialPlacement)); if (!specifiedFallbackPlacements && fallbackAxisSideDirection !== 'none') { fallbackPlacements.push(...getOppositeAxisPlacements(initialPlacement, flipAlignment, fallbackAxisSideDirection, rtl)); } const placements = [initialPlacement, ...fallbackPlacements]; const overflow = await detectOverflow(state, detectOverflowOptions); const overflows = []; let overflowsData = ((_middlewareData$flip = middlewareData.flip) == null ? void 0 : _middlewareData$flip.overflows) || []; if (checkMainAxis) { overflows.push(overflow[side]); } if (checkCrossAxis) { const sides = getAlignmentSides(placement, rects, rtl); overflows.push(overflow[sides[0]], overflow[sides[1]]); } overflowsData = [...overflowsData, { placement, overflows }]; // One or more sides is overflowing. if (!overflows.every(side => side <= 0)) { var _middlewareData$flip2, _overflowsData$filter; const nextIndex = (((_middlewareData$flip2 = middlewareData.flip) == null ? void 0 : _middlewareData$flip2.index) || 0) + 1; const nextPlacement = placements[nextIndex]; if (nextPlacement) { // Try next placement and re-run the lifecycle. return { data: { index: nextIndex, overflows: overflowsData }, reset: { placement: nextPlacement } }; } // First, find the candidates that fit on the mainAxis side of overflow, // then find the placement that fits the best on the main crossAxis side. let resetPlacement = (_overflowsData$filter = overflowsData.filter(d => d.overflows[0] <= 0).sort((a, b) => a.overflows[1] - b.overflows[1])[0]) == null ? void 0 : _overflowsData$filter.placement; // Otherwise fallback. if (!resetPlacement) { switch (fallbackStrategy) { case 'bestFit': { var _overflowsData$map$so; const placement = (_overflowsData$map$so = overflowsData.map(d => [d.placement, d.overflows.filter(overflow => overflow > 0).reduce((acc, overflow) => acc + overflow, 0)]).sort((a, b) => a[1] - b[1])[0]) == null ? void 0 : _overflowsData$map$so[0]; if (placement) { resetPlacement = placement; } break; } case 'initialPlacement': resetPlacement = initialPlacement; break; } } if (placement !== resetPlacement) { return { reset: { placement: resetPlacement } }; } } return {}; } }; }; // For type backwards-compatibility, the `OffsetOptions` type was also // Derivable. async function convertValueToCoords(state, options) { const { placement, platform, elements } = state; const rtl = await (platform.isRTL == null ? void 0 : platform.isRTL(elements.floating)); const side = getSide(placement); const alignment = getAlignment(placement); const isVertical = getSideAxis(placement) === 'y'; const mainAxisMulti = ['left', 'top'].includes(side) ? -1 : 1; const crossAxisMulti = rtl && isVertical ? -1 : 1; const rawValue = evaluate(options, state); // eslint-disable-next-line prefer-const let { mainAxis, crossAxis, alignmentAxis } = typeof rawValue === 'number' ? { mainAxis: rawValue, crossAxis: 0, alignmentAxis: null } : { mainAxis: 0, crossAxis: 0, alignmentAxis: null, ...rawValue }; if (alignment && typeof alignmentAxis === 'number') { crossAxis = alignment === 'end' ? alignmentAxis * -1 : alignmentAxis; } return isVertical ? { x: crossAxis * crossAxisMulti, y: mainAxis * mainAxisMulti } : { x: mainAxis * mainAxisMulti, y: crossAxis * crossAxisMulti }; } /** * Modifies the placement by translating the floating element along the * specified axes. * A number (shorthand for `mainAxis` or distance), or an axes configuration * object may be passed. * @see https://floating-ui.com/docs/offset */ const offset = function (options) { if (options === void 0) { options = 0; } return { name: 'offset', options, async fn(state) { var _middlewareData$offse, _middlewareData$arrow; const { x, y, placement, middlewareData } = state; const diffCoords = await convertValueToCoords(state, options); // If the placement is the same and the arrow caused an alignment offset // then we don't need to change the positioning coordinates. if (placement === ((_middlewareData$offse = middlewareData.offset) == null ? void 0 : _middlewareData$offse.placement) && (_middlewareData$arrow = middlewareData.arrow) != null && _middlewareData$arrow.alignmentOffset) { return {}; } return { x: x + diffCoords.x, y: y + diffCoords.y, data: { ...diffCoords, placement } }; } }; }; /** * Optimizes the visibility of the floating element by shifting it in order to * keep it in view when it will overflow the clipping boundary. * @see https://floating-ui.com/docs/shift */ const shift$1 = function (options) { if (options === void 0) { options = {}; } return { name: 'shift', options, async fn(state) { const { x, y, placement } = state; const { mainAxis: checkMainAxis = true, crossAxis: checkCrossAxis = false, limiter = { fn: _ref => { let { x, y } = _ref; return { x, y }; } }, ...detectOverflowOptions } = evaluate(options, state); const coords = { x, y }; const overflow = await detectOverflow(state, detectOverflowOptions); const crossAxis = getSideAxis(getSide(placement)); const mainAxis = getOppositeAxis(crossAxis); let mainAxisCoord = coords[mainAxis]; let crossAxisCoord = coords[crossAxis]; if (checkMainAxis) { const minSide = mainAxis === 'y' ? 'top' : 'left'; const maxSide = mainAxis === 'y' ? 'bottom' : 'right'; const min = mainAxisCoord + overflow[minSide]; const max = mainAxisCoord - overflow[maxSide]; mainAxisCoord = clamp(min, mainAxisCoord, max); } if (checkCrossAxis) { const minSide = crossAxis === 'y' ? 'top' : 'left'; const maxSide = crossAxis === 'y' ? 'bottom' : 'right'; const min = crossAxisCoord + overflow[minSide]; const max = crossAxisCoord - overflow[maxSide]; crossAxisCoord = clamp(min, crossAxisCoord, max); } const limitedCoords = limiter.fn({ ...state, [mainAxis]: mainAxisCoord, [crossAxis]: crossAxisCoord }); return { ...limitedCoords, data: { x: limitedCoords.x - x, y: limitedCoords.y - y } }; } }; }; /** * Provides data that allows you to change the size of the floating element — * for instance, prevent it from overflowing the clipping boundary or match the * width of the reference element. * @see https://floating-ui.com/docs/size */ const size$1 = function (options) { if (options === void 0) { options = {}; } return { name: 'size', options, async fn(state) { const { placement, rects, platform, elements } = state; const { apply = () => {}, ...detectOverflowOptions } = evaluate(options, state); const overflow = await detectOverflow(state, detectOverflowOptions); const side = getSide(placement); const alignment = getAlignment(placement); const isYAxis = getSideAxis(placement) === 'y'; const { width, height } = rects.floating; let heightSide; let widthSide; if (side === 'top' || side === 'bottom') { heightSide = side; widthSide = alignment === ((await (platform.isRTL == null ? void 0 : platform.isRTL(elements.floating))) ? 'start' : 'end') ? 'left' : 'right'; } else { widthSide = side; heightSide = alignment === 'end' ? 'top' : 'bottom'; } const overflowAvailableHeight = height - overflow[heightSide]; const overflowAvailableWidth = width - overflow[widthSide]; const noShift = !state.middlewareData.shift; let availableHeight = overflowAvailableHeight; let availableWidth = overflowAvailableWidth; if (isYAxis) { const maximumClippingWidth = width - overflow.left - overflow.right; availableWidth = alignment || noShift ? min(overflowAvailableWidth, maximumClippingWidth) : maximumClippingWidth; } else { const maximumClippingHeight = height - overflow.top - overflow.bottom; availableHeight = alignment || noShift ? min(overflowAvailableHeight, maximumClippingHeight) : maximumClippingHeight; } if (noShift && !alignment) { const xMin = max(overflow.left, 0); const xMax = max(overflow.right, 0); const yMin = max(overflow.top, 0); const yMax = max(overflow.bottom, 0); if (isYAxis) { availableWidth = width - 2 * (xMin !== 0 || xMax !== 0 ? xMin + xMax : max(overflow.left, overflow.right)); } else { availableHeight = height - 2 * (yMin !== 0 || yMax !== 0 ? yMin + yMax : max(overflow.top, overflow.bottom)); } } await apply({ ...state, availableWidth, availableHeight }); const nextDimensions = await platform.getDimensions(elements.floating); if (width !== nextDimensions.width || height !== nextDimensions.height) { return { reset: { rects: true } }; } return {}; } }; }; function getNodeName(node) { if (isNode(node)) { return (node.nodeName || '').toLowerCase(); } // Mocked nodes in testing environments may not be instances of Node. By // returning `#document` an infinite loop won't occur. // https://github.com/floating-ui/floating-ui/issues/2317 return '#document'; } function getWindow(node) { var _node$ownerDocument; return (node == null || (_node$ownerDocument = node.ownerDocument) == null ? void 0 : _node$ownerDocument.defaultView) || window; } function getDocumentElement(node) { var _ref; return (_ref = (isNode(node) ? node.ownerDocument : node.document) || window.document) == null ? void 0 : _ref.documentElement; } function isNode(value) { return value instanceof Node || value instanceof getWindow(value).Node; } function isElement(value) { return value instanceof Element || value instanceof getWindow(value).Element; } function isHTMLElement(value) { return value instanceof HTMLElement || value instanceof getWindow(value).HTMLElement; } function isShadowRoot(value) { // Browsers without `ShadowRoot` support. if (typeof ShadowRoot === 'undefined') { return false; } return value instanceof ShadowRoot || value instanceof getWindow(value).ShadowRoot; } function isOverflowElement(element) { const { overflow, overflowX, overflowY, display } = getComputedStyle$1(element); return /auto|scroll|overlay|hidden|clip/.test(overflow + overflowY + overflowX) && !['inline', 'contents'].includes(display); } function isTableElement(element) { return ['table', 'td', 'th'].includes(getNodeName(element)); } function isContainingBlock(element) { const webkit = isWebKit(); const css = getComputedStyle$1(element); // https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block return css.transform !== 'none' || css.perspective !== 'none' || (css.containerType ? css.containerType !== 'normal' : false) || !webkit && (css.backdropFilter ? css.backdropFilter !== 'none' : false) || !webkit && (css.filter ? css.filter !== 'none' : false) || ['transform', 'perspective', 'filter'].some(value => (css.willChange || '').includes(value)) || ['paint', 'layout', 'strict', 'content'].some(value => (css.contain || '').includes(value)); } function getContainingBlock(element) { let currentNode = getParentNode(element); while (isHTMLElement(currentNode) && !isLastTraversableNode(currentNode)) { if (isContainingBlock(currentNode)) { return currentNode; } else { currentNode = getParentNode(currentNode); } } return null; } function isWebKit() { if (typeof CSS === 'undefined' || !CSS.supports) return false; return CSS.supports('-webkit-backdrop-filter', 'none'); } function isLastTraversableNode(node) { return ['html', 'body', '#document'].includes(getNodeName(node)); } function getComputedStyle$1(element) { return getWindow(element).getComputedStyle(element); } function getNodeScroll(element) { if (isElement(element)) { return { scrollLeft: element.scrollLeft, scrollTop: element.scrollTop }; } return { scrollLeft: element.pageXOffset, scrollTop: element.pageYOffset }; } function getParentNode(node) { if (getNodeName(node) === 'html') { return node; } const result = // Step into the shadow DOM of the parent of a slotted node. node.assignedSlot || // DOM Element detected. node.parentNode || // ShadowRoot detected. isShadowRoot(node) && node.host || // Fallback. getDocumentElement(node); return isShadowRoot(result) ? result.host : result; } function getNearestOverflowAncestor(node) { const parentNode = getParentNode(node); if (isLastTraversableNode(parentNode)) { return node.ownerDocument ? node.ownerDocument.body : node.body; } if (isHTMLElement(parentNode) && isOverflowElement(parentNode)) { return parentNode; } return getNearestOverflowAncestor(parentNode); } function getOverflowAncestors(node, list, traverseIframes) { var _node$ownerDocument2; if (list === void 0) { list = []; } if (traverseIframes === void 0) { traverseIframes = true; } const scrollableAncestor = getNearestOverflowAncestor(node); const isBody = scrollableAncestor === ((_node$ownerDocument2 = node.ownerDocument) == null ? void 0 : _node$ownerDocument2.body); const win = getWindow(scrollableAncestor); if (isBody) { return list.concat(win, win.visualViewport || [], isOverflowElement(scrollableAncestor) ? scrollableAncestor : [], win.frameElement && traverseIframes ? getOverflowAncestors(win.frameElement) : []); } return list.concat(scrollableAncestor, getOverflowAncestors(scrollableAncestor, [], traverseIframes)); } function getCssDimensions(element) { const css = getComputedStyle$1(element); // In testing environments, the `width` and `height` properties are empty // strings for SVG elements, returning NaN. Fallback to `0` in this case. let width = parseFloat(css.width) || 0; let height = parseFloat(css.height) || 0; const hasOffset = isHTMLElement(element); const offsetWidth = hasOffset ? element.offsetWidth : width; const offsetHeight = hasOffset ? element.offsetHeight : height; const shouldFallback = round(width) !== offsetWidth || round(height) !== offsetHeight; if (shouldFallback) { width = offsetWidth; height = offsetHeight; } return { width, height, $: shouldFallback }; } function unwrapElement(element) { return !isElement(element) ? element.contextElement : element; } function getScale(element) { const domElement = unwrapElement(element); if (!isHTMLElement(domElement)) { return createCoords(1); } const rect = domElement.getBoundingClientRect(); const { width, height, $ } = getCssDimensions(domElement); let x = ($ ? round(rect.width) : rect.width) / width; let y = ($ ? round(rect.height) : rect.height) / height; // 0, NaN, or Infinity should always fallback to 1. if (!x || !Number.isFinite(x)) { x = 1; } if (!y || !Number.isFinite(y)) { y = 1; } return { x, y }; } const noOffsets = /*#__PURE__*/createCoords(0); function getVisualOffsets(element) { const win = getWindow(element); if (!isWebKit() || !win.visualViewport) { return noOffsets; } return { x: win.visualViewport.offsetLeft, y: win.visualViewport.offsetTop }; } function shouldAddVisualOffsets(element, isFixed, floatingOffsetParent) { if (isFixed === void 0) { isFixed = false; } if (!floatingOffsetParent || isFixed && floatingOffsetParent !== getWindow(element)) { return false; } return isFixed; } function getBoundingClientRect(element, includeScale, isFixedStrategy, offsetParent) { if (includeScale === void 0) { includeScale = false; } if (isFixedStrategy === void 0) { isFixedStrategy = false; } const clientRect = element.getBoundingClientRect(); const domElement = unwrapElement(element); let scale = createCoords(1); if (includeScale) { if (offsetParent) { if (isElement(offsetParent)) { scale = getScale(offsetParent); } } else { scale = getScale(element); } } const visualOffsets = shouldAddVisualOffsets(domElement, isFixedStrategy, offsetParent) ? getVisualOffsets(domElement) : createCoords(0); let x = (clientRect.left + visualOffsets.x) / scale.x; let y = (clientRect.top + visualOffsets.y) / scale.y; let width = clientRect.width / scale.x; let height = clientRect.height / scale.y; if (domElement) { const win = getWindow(domElement); const offsetWin = offsetParent && isElement(offsetParent) ? getWindow(offsetParent) : offsetParent; let currentIFrame = win.frameElement; while (currentIFrame && offsetParent && offsetWin !== win) { const iframeScale = getScale(currentIFrame); const iframeRect = currentIFrame.getBoundingClientRect(); const css = getComputedStyle$1(currentIFrame); const left = iframeRect.left + (currentIFrame.clientLeft + parseFloat(css.paddingLeft)) * iframeScale.x; const top = iframeRect.top + (currentIFrame.clientTop + parseFloat(css.paddingTop)) * iframeScale.y; x *= iframeScale.x; y *= iframeScale.y; width *= iframeScale.x; height *= iframeScale.y; x += left; y += top; currentIFrame = getWindow(currentIFrame).frameElement; } } return rectToClientRect({ width, height, x, y }); } function convertOffsetParentRelativeRectToViewportRelativeRect(_ref) { let { rect, offsetParent, strategy } = _ref; const isOffsetParentAnElement = isHTMLElement(offsetParent); const documentElement = getDocumentElement(offsetParent); if (offsetParent === documentElement) { return rect; } let scroll = { scrollLeft: 0, scrollTop: 0 }; let scale = createCoords(1); const offsets = createCoords(0); if (isOffsetParentAnElement || !isOffsetParentAnElement && strategy !== 'fixed') { if (getNodeName(offsetParent) !== 'body' || isOverflowElement(documentElement)) { scroll = getNodeScroll(offsetParent); } if (isHTMLElement(offsetParent)) { const offsetRect = getBoundingClientRect(offsetParent); scale = getScale(offsetParent); offsets.x = offsetRect.x + offsetParent.clientLeft; offsets.y = offsetRect.y + offsetParent.clientTop; } } return { width: rect.width * scale.x, height: rect.height * scale.y, x: rect.x * scale.x - scroll.scrollLeft * scale.x + offsets.x, y: rect.y * scale.y - scroll.scrollTop * scale.y + offsets.y }; } function getClientRects(element) { return Array.from(element.getClientRects()); } function getWindowScrollBarX(element) { // If has a CSS width greater than the viewport, then this will be // incorrect for RTL. return getBoundingClientRect(getDocumentElement(element)).left + getNodeScroll(element).scrollLeft; } // Gets the entire size of the scrollable document area, even extending outside // of the `` and `` rect bounds if horizontally scrollable. function getDocumentRect(element) { const html = getDocumentElement(element); const scroll = getNodeScroll(element); const body = element.ownerDocument.body; const width = max(html.scrollWidth, html.clientWidth, body.scrollWidth, body.clientWidth); const height = max(html.scrollHeight, html.clientHeight, body.scrollHeight, body.clientHeight); let x = -scroll.scrollLeft + getWindowScrollBarX(element); const y = -scroll.scrollTop; if (getComputedStyle$1(body).direction === 'rtl') { x += max(html.clientWidth, body.clientWidth) - width; } return { width, height, x, y }; } function getViewportRect(element, strategy) { const win = getWindow(element); const html = getDocumentElement(element); const visualViewport = win.visualViewport; let width = html.clientWidth; let height = html.clientHeight; let x = 0; let y = 0; if (visualViewport) { width = visualViewport.width; height = visualViewport.height; const visualViewportBased = isWebKit(); if (!visualViewportBased || visualViewportBased && strategy === 'fixed') { x = visualViewport.offsetLeft; y = visualViewport.offsetTop; } } return { width, height, x, y }; } // Returns the inner client rect, subtracting scrollbars if present. function getInnerBoundingClientRect(element, strategy) { const clientRect = getBoundingClientRect(element, true, strategy === 'fixed'); const top = clientRect.top + element.clientTop; const left = clientRect.left + element.clientLeft; const scale = isHTMLElement(element) ? getScale(element) : createCoords(1); const width = element.clientWidth * scale.x; const height = element.clientHeight * scale.y; const x = left * scale.x; const y = top * scale.y; return { width, height, x, y }; } function getClientRectFromClippingAncestor(element, clippingAncestor, strategy) { let rect; if (clippingAncestor === 'viewport') { rect = getViewportRect(element, strategy); } else if (clippingAncestor === 'document') { rect = getDocumentRect(getDocumentElement(element)); } else if (isElement(clippingAncestor)) { rect = getInnerBoundingClientRect(clippingAncestor, strategy); } else { const visualOffsets = getVisualOffsets(element); rect = { ...clippingAncestor, x: clippingAncestor.x - visualOffsets.x, y: clippingAncestor.y - visualOffsets.y }; } return rectToClientRect(rect); } function hasFixedPositionAncestor(element, stopNode) { const parentNode = getParentNode(element); if (parentNode === stopNode || !isElement(parentNode) || isLastTraversableNode(parentNode)) { return false; } return getComputedStyle$1(parentNode).position === 'fixed' || hasFixedPositionAncestor(parentNode, stopNode); } // A "clipping ancestor" is an `overflow` element with the characteristic of // clipping (or hiding) child elements. This returns all clipping ancestors // of the given element up the tree. function getClippingElementAncestors(element, cache) { const cachedResult = cache.get(element); if (cachedResult) { return cachedResult; } let result = getOverflowAncestors(element, [], false).filter(el => isElement(el) && getNodeName(el) !== 'body'); let currentContainingBlockComputedStyle = null; const elementIsFixed = getComputedStyle$1(element).position === 'fixed'; let currentNode = elementIsFixed ? getParentNode(element) : element; // https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block while (isElement(currentNode) && !isLastTraversableNode(currentNode)) { const computedStyle = getComputedStyle$1(currentNode); const currentNodeIsContaining = isContainingBlock(currentNode); if (!currentNodeIsContaining && computedStyle.position === 'fixed') { currentContainingBlockComputedStyle = null; } const shouldDropCurrentNode = elementIsFixed ? !currentNodeIsContaining && !currentContainingBlockComputedStyle : !currentNodeIsContaining && computedStyle.position === 'static' && !!currentContainingBlockComputedStyle && ['absolute', 'fixed'].includes(currentContainingBlockComputedStyle.position) || isOverflowElement(currentNode) && !currentNodeIsContaining && hasFixedPositionAncestor(element, currentNode); if (shouldDropCurrentNode) { // Drop non-containing blocks. result = result.filter(ancestor => ancestor !== currentNode); } else { // Record last containing block for next iteration. currentContainingBlockComputedStyle = computedStyle; } currentNode = getParentNode(currentNode); } cache.set(element, result); return result; } // Gets the maximum area that the element is visible in due to any number of // clipping ancestors. function getClippingRect(_ref) { let { element, boundary, rootBoundary, strategy } = _ref; const elementClippingAncestors = boundary === 'clippingAncestors' ? getClippingElementAncestors(element, this._c) : [].concat(boundary); const clippingAncestors = [...elementClippingAncestors, rootBoundary]; const firstClippingAncestor = clippingAncestors[0]; const clippingRect = clippingAncestors.reduce((accRect, clippingAncestor) => { const rect = getClientRectFromClippingAncestor(element, clippingAncestor, strategy); accRect.top = max(rect.top, accRect.top); accRect.right = min(rect.right, accRect.right); accRect.bottom = min(rect.bottom, accRect.bottom); accRect.left = max(rect.left, accRect.left); return accRect; }, getClientRectFromClippingAncestor(element, firstClippingAncestor, strategy)); return { width: clippingRect.right - clippingRect.left, height: clippingRect.bottom - clippingRect.top, x: clippingRect.left, y: clippingRect.top }; } function getDimensions(element) { const { width, height } = getCssDimensions(element); return { width, height }; } function getRectRelativeToOffsetParent(element, offsetParent, strategy) { const isOffsetParentAnElement = isHTMLElement(offsetParent); const documentElement = getDocumentElement(offsetParent); const isFixed = strategy === 'fixed'; const rect = getBoundingClientRect(element, true, isFixed, offsetParent); let scroll = { scrollLeft: 0, scrollTop: 0 }; const offsets = createCoords(0); if (isOffsetParentAnElement || !isOffsetParentAnElement && !isFixed) { if (getNodeName(offsetParent) !== 'body' || isOverflowElement(documentElement)) { scroll = getNodeScroll(offsetParent); } if (isOffsetParentAnElement) { const offsetRect = getBoundingClientRect(offsetParent, true, isFixed, offsetParent); offsets.x = offsetRect.x + offsetParent.clientLeft; offsets.y = offsetRect.y + offsetParent.clientTop; } else if (documentElement) { offsets.x = getWindowScrollBarX(documentElement); } } return { x: rect.left + scroll.scrollLeft - offsets.x, y: rect.top + scroll.scrollTop - offsets.y, width: rect.width, height: rect.height }; } function getTrueOffsetParent(element, polyfill) { if (!isHTMLElement(element) || getComputedStyle$1(element).position === 'fixed') { return null; } if (polyfill) { return polyfill(element); } return element.offsetParent; } // Gets the closest ancestor positioned element. Handles some edge cases, // such as table ancestors and cross browser bugs. function getOffsetParent(element, polyfill) { const window = getWindow(element); if (!isHTMLElement(element)) { return window; } let offsetParent = getTrueOffsetParent(element, polyfill); while (offsetParent && isTableElement(offsetParent) && getComputedStyle$1(offsetParent).position === 'static') { offsetParent = getTrueOffsetParent(offsetParent, polyfill); } if (offsetParent && (getNodeName(offsetParent) === 'html' || getNodeName(offsetParent) === 'body' && getComputedStyle$1(offsetParent).position === 'static' && !isContainingBlock(offsetParent))) { return window; } return offsetParent || getContainingBlock(element) || window; } const getElementRects = async function (_ref) { let { reference, floating, strategy } = _ref; const getOffsetParentFn = this.getOffsetParent || getOffsetParent; const getDimensionsFn = this.getDimensions; return { reference: getRectRelativeToOffsetParent(reference, await getOffsetParentFn(floating), strategy), floating: { x: 0, y: 0, ...(await getDimensionsFn(floating)) } }; }; function isRTL(element) { return getComputedStyle$1(element).direction === 'rtl'; } const platform = { convertOffsetParentRelativeRectToViewportRelativeRect, getDocumentElement, getClippingRect, getOffsetParent, getElementRects, getClientRects, getDimensions, getScale, isElement, isRTL }; // https://samthor.au/2021/observing-dom/ function observeMove(element, onMove) { let io = null; let timeoutId; const root = getDocumentElement(element); function cleanup() { clearTimeout(timeoutId); io && io.disconnect(); io = null; } function refresh(skip, threshold) { if (skip === void 0) { skip = false; } if (threshold === void 0) { threshold = 1; } cleanup(); const { left, top, width, height } = element.getBoundingClientRect(); if (!skip) { onMove(); } if (!width || !height) { return; } const insetTop = floor(top); const insetRight = floor(root.clientWidth - (left + width)); const insetBottom = floor(root.clientHeight - (top + height)); const insetLeft = floor(left); const rootMargin = -insetTop + "px " + -insetRight + "px " + -insetBottom + "px " + -insetLeft + "px"; const options = { rootMargin, threshold: max(0, min(1, threshold)) || 1 }; let isFirstUpdate = true; function handleObserve(entries) { const ratio = entries[0].intersectionRatio; if (ratio !== threshold) { if (!isFirstUpdate) { return refresh(); } if (!ratio) { timeoutId = setTimeout(() => { refresh(false, 1e-7); }, 100); } else { refresh(false, ratio); } } isFirstUpdate = false; } // Older browsers don't support a `document` as the root and will throw an // error. try { io = new IntersectionObserver(handleObserve, { ...options, // Handle