Greasy Fork

视频网站自动网页全屏|倍速播放

支持哔哩哔哩、B站直播、腾讯视频、优酷视频、爱奇艺、芒果TV、搜狐视频、AcFun弹幕网自动网页全屏;快捷键切换:全屏(F)、网页全屏(P)、下一个视频(N)、弹幕开关(D);支持任意视频倍速播放,提示记忆倍速;B站播放完自动退出网页全屏和取消连播。

目前为 2025-05-07 提交的版本。查看 最新版本

// ==UserScript==
// @name         视频网站自动网页全屏|倍速播放
// @namespace    http://tampermonkey.net/
// @version      2.7.5
// @author       Feny
// @description  支持哔哩哔哩、B站直播、腾讯视频、优酷视频、爱奇艺、芒果TV、搜狐视频、AcFun弹幕网自动网页全屏;快捷键切换:全屏(F)、网页全屏(P)、下一个视频(N)、弹幕开关(D);支持任意视频倍速播放,提示记忆倍速;B站播放完自动退出网页全屏和取消连播。
// @license      GPL-3.0-only
// @icon         data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAqdJREFUWEftl91LFFEYxp/3jB9ESZjtSl51F1RUSgRCF/kHlF1IhiFhF65dqEQkBUErdJMStBukGwQre2NZUiCRqUiURkW65mIfqGUFsW6Ii0jY7p4Tc3Rqd5zaGVldAudynve8z28e3jMzh5Dmi1R/V0vQyRRWxgWG6x22SrcnOAhQcQIbwVtXba8y1EANSpS1xzJin5c/Dz+jRDPvGWoErwRw35zuh8ChpcXXFjbwi9k/WADA9viGgovGnxtFs6EmcApMvCdBA3oIIirl4N8NNQngmRYJiwTOE7EHHLERAmXFawQ6AdCQkRbjsZIMUvIFoV0HMSsEDjCgSK8tJqAHAEDAMWLKLOexx8tiVVDEhLLVQAtzRPcwKOUANSWCw1/rsBe6PcFz8dpfAdTFgtF+EmIvBG7pID7mZNl2zkVCFQbahzqHfYerddpNhFpdsnfqauzl8ZoEuO4JXdIKOefynnZlimxXhBbqjTZL/el8pzrAVjTGmKh12Bq1ddJs974abQDXfFMuAhQ6EodwDTHWAf6/BAoK8nD0cDEKtuVhyD+OzvvLXnyWJshyApedJ1F65M9n4tlAAF5fL168fGfJWCu2DDA61GpodLvjCdp8vfjyNWQJJGUAquvMzBzafD0yEc65KZCUAmiOo4FPEqS753VSiFUB0FxbPF244en6J8SqAoTD8zhYcjZ9AP6RCVRWNacHYPD5GJqudmBi8tvaAkxNBeUuuNv5NOkAqgUpm4FIJCrfA+r0z4bnTZmvCKCv+wrsts0JBg8fvZLGY28NfoqToFhOoOJ4CS40lMu2I28mpXFP37DpJ9YXWgZQG+Tm5mBL7qakA2aGakUAZhqbrVkH0BLoB34fzcyml5K6pd/yaicRlQlgV0q6mmwitMOpyfpVKfsFya4w73cz9xQAAAAASUVORK5CYII=
// @homepage     https://github.com/xFeny/UserScript/tree/main/monkey-web-fullscreen
// @include      *://pages.iqiyi.com/p/zy/*
// @include      *://www.ezdmw.site/Index/video/*
// @include      *://player.ezdmw.com/danmuku/*
// @include      *://*bimiacg*.net/*/play*
// @include      *://acgfta.com/play*
// @include      *://ppoft.com/play*
// @match        *://tv.sohu.com/v/*
// @match        *://www.mgtv.com/b/*
// @match        *://www.acfun.cn/v/*
// @match        *://www.iqiyi.com/v_*
// @match        *://v.pptv.com/show/*
// @match        *://v.qq.com/x/page/*
// @match        *://v.douyu.com/show/*
// @match        *://v.qq.com/x/cover/*
// @match        *://live.bilibili.com/*
// @match        *://v.youku.com/video?*
// @match        *://haokan.baidu.com/v?*
// @match        *://live.acfun.cn/live/*
// @match        *://www.acfun.cn/bangumi/*
// @match        *://www.bilibili.com/list/*
// @match        *://www.bilibili.com/video/*
// @match        *://www.bilibili.com/*/play/*
// @match        *://v.qq.com/live/p/newtopic/*
// @match        *://www.bilibili.com/festival/*
// @require      https://unpkg.com/[email protected]/notyf.min.js
// @require      data:application/javascript,%3Bwindow.notyf%3D%7BNotyf%7D%3B
// @require      https://unpkg.com/[email protected]/dist/sweetalert2.min.js
// @require      data:application/javascript,%3Bwindow.sweetalert2%3DSwal%3B
// @resource     notyf/notyf.min.css  https://unpkg.com/[email protected]/notyf.min.css
// @resource     sweetalert2          https://unpkg.com/[email protected]/dist/sweetalert2.min.css
// @grant        GM_addStyle
// @grant        GM_addValueChangeListener
// @grant        GM_deleteValue
// @grant        GM_getResourceText
// @grant        GM_getValue
// @grant        GM_info
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_unregisterMenuCommand
// @grant        unsafeWindow
// @note         *://*/*
// ==/UserScript==

(e=>{if(typeof GM_addStyle=="function"){GM_addStyle(e);return}const o=document.createElement("style");o.textContent=e,document.head.append(o)})(' @charset "UTF-8";.showToast{color:#fff!important;font-size:13.5px!important;padding:5px 15px!important;border-radius:5px!important;position:absolute!important;z-index:2147483647!important;font-weight:400!important;transition:opacity .3s ease-in;background:#000000bf!important}#bilibili-player .bpx-player-toast-wrap,#bilibili-player .bpx-player-cmd-dm-wrap,#bilibili-player .bpx-player-dialog-wrap,.live-room-app #sidebar-vm,.live-room-app #prehold-nav-vm,.live-room-app #shop-popover-vm,.login-tip{display:none!important}.monkey-web-fullscreen h2{font-size:24px}.monkey-web-fullscreen h4{color:red;margin:0 auto}.monkey-web-fullscreen p{color:#999;font-size:12px}.monkey-web-fullscreen #picker-chain{width:25em;height:auto;font-size:14px;margin-bottom:0;min-height:10em;resize:vertical}.monkey-web-fullscreen .swal2-confirm{background-color:#7066e0!important}.monkey-web-fullscreen .swal2-deny{background-color:#dc3741!important}.notyf__message{overflow:hidden;display:-webkit-box;line-clamp:4;-webkit-line-clamp:4;text-overflow:ellipsis;-webkit-box-orient:vertical} ');

(function (notyf, Swal) {
  'use strict';

  const cssLoader = (e) => {
    const t = GM_getResourceText(e);
    return GM_addStyle(t), t;
  };
  cssLoader("notyf/notyf.min.css");
  const positions = Object.freeze({
    bottomLeft: "bottom: 17%; left: 10px;",
    bottomRight: "bottom: 17%; right: 10px;",
    center: "top: 50%; left: 50%; transform: translate(-50%, -50%);"
  });
  const ONE_SECOND = 1e3;
  const constants = Object.freeze({
    EMPTY: "",
    DEF_PLAY_RATE: 1,
    MAX_PLAY_RATE: 16,
    ONE_SEC: ONE_SECOND,
    SHOW_TOAST_TIME: ONE_SECOND * 5,
    SHOW_TOAST_POSITION: positions.bottomLeft,
    MSG_SOURCE: "SCRIPTS_AUTO_WEB_FULLSCREEN",
    QQ_VID_REG: /v.qq.com\/x/,
    ACFUN_VID_REG: /acfun.cn\/v/,
    IQIYI_VID_REG: /iqiyi.com\/v_*/,
    BILI_VID_REG: /bilibili.com\/video/,
    SYMBOL: Object.freeze({
      ADD: "+",
      SUBTRACT: "-",
      MULTIPLY: "×",
      DIVIDE: "÷"
    })
  });
  function isElement(node) {
    return node.nodeType === Node.ELEMENT_NODE;
  }
  function isDocument(node) {
    return node.nodeType === Node.DOCUMENT_NODE;
  }
  function* getShadowRoots(node, deep) {
    if (!isElement(node) && !isDocument(node)) {
      return;
    }
    const doc = isDocument(node) ? node : node.getRootNode({ composed: true });
    if (isElement(node) && node.shadowRoot) {
      yield node.shadowRoot;
    }
    const toWalk = [node];
    let currentNode = void 0;
    while (currentNode = toWalk.pop()) {
      const walker = doc.createTreeWalker(currentNode, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_DOCUMENT_FRAGMENT, {
        acceptNode(node2) {
          if (isElement(node2) && node2.shadowRoot) {
            return NodeFilter.FILTER_ACCEPT;
          }
          return NodeFilter.FILTER_SKIP;
        }
      });
      let walkerNode = walker.nextNode();
      while (walkerNode) {
        if (isElement(walkerNode) && walkerNode.shadowRoot) {
          {
            toWalk.push(walkerNode.shadowRoot);
          }
          yield walkerNode.shadowRoot;
        }
        walkerNode = walker.nextNode();
      }
    }
    return;
  }
  function queryCrossBoundary(selector, subject = document) {
    const immediate = subject.querySelector(selector);
    if (immediate) {
      return immediate;
    }
    const shadowRoots = [...getShadowRoots(subject)];
    for (const root of shadowRoots) {
      const child = root.querySelector(selector);
      if (child) {
        return child;
      }
    }
    return null;
  }
  function queryAllCrossBoundary(selector, subject = document) {
    const results = [...subject.querySelectorAll(selector)];
    const shadowRoots = [...getShadowRoots(subject)];
    for (const root of shadowRoots) {
      const children = root.querySelectorAll(selector);
      for (const child of children) {
        results.push(child);
      }
    }
    return results;
  }
  function querySelector(selectors, subject = document, _options) {
    const selectorList = Array.isArray(selectors) ? selectors : [selectors];
    if (selectorList.length === 0) {
      return null;
    }
    let currentSubjects = [subject];
    let result = null;
    for (const selector of selectorList) {
      const newSubjects = [];
      for (const currentSubject of currentSubjects) {
        const child = queryCrossBoundary(selector, currentSubject);
        if (child) {
          result = child;
          newSubjects.push(child);
        }
      }
      if (newSubjects.length === 0) {
        return null;
      }
      currentSubjects = newSubjects;
    }
    return result;
  }
  function querySelectorAll(selectors, subject = document, _options) {
    const selectorList = Array.isArray(selectors) ? selectors : [selectors];
    if (selectorList.length === 0) {
      return [];
    }
    let currentSubjects = [subject];
    let results = [];
    for (const selector of selectorList) {
      const newSubjects = [];
      for (const currentSubject of currentSubjects) {
        const children = queryAllCrossBoundary(selector, currentSubject);
        for (const child of children) {
          newSubjects.push(child);
        }
      }
      if (newSubjects.length === 0) {
        return [];
      }
      currentSubjects = newSubjects;
      results = newSubjects;
    }
    return results;
  }
  var _GM_addValueChangeListener = /* @__PURE__ */ (() => typeof GM_addValueChangeListener != "undefined" ? GM_addValueChangeListener : void 0)();
  var _GM_deleteValue = /* @__PURE__ */ (() => typeof GM_deleteValue != "undefined" ? GM_deleteValue : void 0)();
  var _GM_getValue = /* @__PURE__ */ (() => typeof GM_getValue != "undefined" ? GM_getValue : void 0)();
  var _GM_info = /* @__PURE__ */ (() => typeof GM_info != "undefined" ? GM_info : void 0)();
  var _GM_registerMenuCommand = /* @__PURE__ */ (() => typeof GM_registerMenuCommand != "undefined" ? GM_registerMenuCommand : void 0)();
  var _GM_setValue = /* @__PURE__ */ (() => typeof GM_setValue != "undefined" ? GM_setValue : void 0)();
  var _GM_unregisterMenuCommand = /* @__PURE__ */ (() => typeof GM_unregisterMenuCommand != "undefined" ? GM_unregisterMenuCommand : void 0)();
  var _unsafeWindow = /* @__PURE__ */ (() => typeof unsafeWindow != "undefined" ? unsafeWindow : void 0)();
  const { ONE_SEC: ONE_SEC$1, MSG_SOURCE: MSG_SOURCE$1 } = constants;
  const Tools = {
    isTopWin: () => window.top === window,
    noNumber: (str) => !/\d/.test(str),
    isNumber: (str) => /^[0-9]$/.test(str),
    scrollTop: (top) => _unsafeWindow.top.scrollTo({ top }),
    query: (selector, context) => querySelector(selector, context),
    querys: (selector, context) => querySelectorAll(selector, context),
    validDuration: (video) => !isNaN(video.duration) && video.duration !== Infinity,
    triggerClick: (ele) => ele?.dispatchEvent(new MouseEvent("click", { bubbles: true })),
    postMessage: (win = null, data) => win?.postMessage({ source: MSG_SOURCE$1, ...data }, "*"),
    isVisible: (ele) => !!(ele?.offsetWidth || ele?.offsetHeight || ele?.getClientRects().length),
    log: (...data) => console.log(...["%c======= 脚本日志 =======\n\n", "color:green;font-size:14px;", ...data, "\n\n"]),
    alert: (...data) => window.alert(data.join(" ")),
    preventDefault(event) {
      event.preventDefault();
      event.stopPropagation();
      event.stopImmediatePropagation();
    },
    notyf(msg, isError = false) {
      const notyf$1 = new notyf.Notyf({
        duration: ONE_SEC$1 * 3,
        position: { x: "center", y: "top" }
      });
      isError ? notyf$1.error(msg) : notyf$1.success(msg);
      return false;
    },
    getFrames() {
      return this.querys("iframe:not([src=''], [src='#'], [id='buffer'], [id='install'])");
    },
    postMsgToFrames(data) {
      this.getFrames().forEach((iframe) => this.postMessage(iframe?.contentWindow, data));
    },
    debounce(fn, delay = ONE_SEC$1) {
      let timer;
      return function() {
        if (timer) clearTimeout(timer);
        timer = setTimeout(() => fn.apply(this, arguments), delay);
      };
    },
    getElementRect(element) {
      return element?.getBoundingClientRect();
    },
    getElementCenterPoint(element) {
      const { top, left, width, height } = this.getElementRect(element);
      return { centerX: left + width / 2, centerY: top + height / 2 };
    },
    isPointInElementRect(pointX, pointY, element) {
      const { top, left, right, bottom } = this.getElementRect(element);
      return pointX >= left && pointX <= right && pointY >= top && pointY <= bottom;
    },
    triggerMousemove(element) {
      const offsetWidth = element.offsetWidth;
      const { centerY } = this.getElementCenterPoint(element);
      for (let clientX = 0; clientX < offsetWidth; clientX += 10) {
        this.dispatchMousemove(element, clientX, centerY);
      }
    },
    dispatchMousemove(element, clientX, clientY) {
      const dict = { clientX, clientY, bubbles: true };
      const mousemove = new MouseEvent("mousemove", dict);
      element.dispatchEvent(mousemove);
    },
    triggerHoverEvent(element) {
      if (!element) return;
      this.log("网页全屏元素:", element);
      const { centerX, centerY } = this.getElementCenterPoint(element);
      const dict = { clientX: centerX, clientY: centerY, bubbles: true };
      const mouseover = new MouseEvent("mouseover", dict);
      element?.dispatchEvent(mouseover);
    },
    triggerEscapeEvent() {
      const dict = { key: "Escape", keyCode: 27, bubbles: true };
      const keydown = new KeyboardEvent("keydown", dict);
      document.body?.dispatchEvent(keydown);
    },
    createObserver(target, callback) {
      target = target instanceof HTMLElement ? target : this.query(target);
      const options = { attributes: true, childList: true, subtree: true };
      const observer = new MutationObserver(callback);
      observer.observe(target, options);
      return observer;
    },
    closest(element, selector, maxLevel = 3) {
      let currLevel = 0;
      while (element && currLevel < maxLevel) {
        if (element.matches(selector)) return element;
        element = element.parentElement;
        currLevel++;
      }
      return null;
    },
    findSiblingInParent(element, selector, maxLevel = 3) {
      let currLevel = 0;
      let currParent = element.parentElement;
      while (currParent && currLevel < maxLevel) {
        const sibs = currParent.children;
        for (let sib of sibs) {
          if (sib !== element && sib.matches(selector)) return sib;
        }
        currParent = currParent.parentElement;
        currLevel++;
      }
      return null;
    },
    haveSiblings(element) {
      return element?.parentElement?.children.length > 1;
    },
    extractNumbers(str) {
      if (!str) return [];
      const numbers = str.match(/\d+/g);
      return numbers ? numbers.map(Number) : [];
    },
    getParentChain(element) {
      let parents = [];
      let current = element;
      while (current && current.tagName !== "BODY") {
        const tagInfo = this.getTagInfo(current);
        parents.unshift(tagInfo);
        if (current.id && this.noNumber(current.id)) break;
        current = current.parentNode;
      }
      return parents.join(" > ");
    },
    getTagInfo(ele) {
      if (ele.id && this.noNumber(ele.id)) return `#${ele.id}`;
      const classList = ele.classList;
      let tagInfo = ele.tagName.toLowerCase();
      if (!!classList.length) {
        if (/[:\[\]]/.test(ele.className)) {
          tagInfo += `[class="${ele.className}"]`;
        } else {
          const filterClass = Array.from(classList).filter((cls) => this.noNumber(cls));
          if (!!filterClass.length) tagInfo += `.${filterClass.join(".")}`;
        }
      }
      return tagInfo;
    }
  };
  const { EMPTY: EMPTY$2, QQ_VID_REG, BILI_VID_REG, IQIYI_VID_REG, ACFUN_VID_REG } = constants;
  const matches = _GM_info.script.matches.filter((match) => match !== "*://*/*").map((match) => new RegExp(match.replace(/\*/g, "\\S+")));
  const webSite = {
    isDouyu: () => location.host === "v.douyu.com",
    isBili: () => BILI_VID_REG.test(location.href),
    isTencent: () => QQ_VID_REG.test(location.href),
    isIqiyi: () => IQIYI_VID_REG.test(location.href),
    isAcFun: () => ACFUN_VID_REG.test(location.href),
    isLivePage: () => location.href.includes("live"),
    isBiliLive: () => location.host === "live.bilibili.com",
    inMatches: () => matches.some((matche) => matche.test(location.href.replace(location.search, EMPTY$2)))
  };
  const selectorConfig = {
    "live.bilibili.com": { webfull: "#businessContainerElement" },
    "www.bilibili.com": { webfull: "div[aria-label='网页全屏']", next: ".bpx-player-ctrl-next" },
    "live.acfun.cn": { full: ".fullscreen-screen", webfull: ".fullscreen-web", danmaku: ".danmaku-enabled" },
    "tv.sohu.com": { full: ".x-fullscreen-btn", webfull: ".x-pagefs-btn", danmaku: ".tm-tmbtn", next: ".x-next-btn" },
    "haokan.baidu.com": { full: ".art-icon-fullscreen", webfull: ".art-control-fullscreenWeb", next: ".art-control-next" },
    "www.iqiyi.com": { full: ".iqp-btn-fullscreen", webfull: ".iqp-btn-webscreen", danmaku: "#barrage_switch", next: ".iqp-btn-next" },
    "www.mgtv.com": { full: ".fullscreenBtn i", webfull: ".webfullscreenBtn i", danmaku: "div[class*='danmuSwitch']", next: ".icon-next" },
    "v.qq.com": { full: ".txp_btn_fullscreen", webfull: "div[aria-label='网页全屏']", danmaku: ".barrage-switch", next: ".txp_btn_next_u" },
    "v.pptv.com": { full: ".w-zoom-container > div", webfull: ".w-expand-container > div", danmaku: ".w-barrage", next: ".w-next-container" },
    "www.acfun.cn": { full: ".fullscreen-screen", webfull: ".fullscreen-web", danmaku: ".danmaku-enabled", next: ".btn-next-part .control-btn" },
    "v.youku.com": { full: "#fullscreen-icon", webfull: "#webfullscreen-icon", danmaku: "div[class*='switch-img_12hDa turn-']", next: ".kui-next-icon-0" },
    "v.douyu.com": { full: ".ControllerBar-WindowFull-Icon", webfull: ".ControllerBar-PageFull-Icon", danmaku: ".BarrageSwitch-icon" }
  };
  const douyu = {
    play: () => Tools.query(".ControllerBarPlay")?.click(),
    pause: () => Tools.query(".ControllerBarStop")?.click(),
    addStyle(ele) {
      let style = Tools.query("style", ele);
      if (style) return;
      style = document.createElement("style");
      style.textContent = ".showToast{color:#fff!important;font-size:13.5px!important;padding:5px 15px!important;border-radius:5px!important;position:absolute!important;z-index:2147483647!important;font-weight:400!important;transition:opacity 300ms ease-in;background:rgba(0,0,0,.75)!important}";
      ele.appendChild(style);
    }
  };
  const VideoListenerHandler = {
    loadedmetadata() {
      this.volume = 1;
      this.isToastShown = false;
      this.isWebFullScreen = false;
    },
    loadeddata() {
      this.volume = 1;
      this.isToastShown = false;
      this.isWebFullScreen = false;
    },
    timeupdate() {
      if (isNaN(this.duration)) return;
      App.changeVideoInfo(this);
      App.experimentWebFullScreen(this);
      App.currVideoUseCachePlayRate(this);
    },
    canplay() {
      webSite.isDouyu() ? douyu.play() : this.play();
    },
    play() {
      this.isEnded = false;
      App.webFullScreen(this);
    },
    ended() {
      this.isEnded = true;
      this.isToastShown = false;
      if (!webSite.isBili() && !webSite.isAcFun()) return;
      const pod = Tools.query(".video-pod");
      const pods = Tools.querys('.video-pod .switch-btn:not(.on), .video-pod__item:last-of-type[data-scrolled="true"]');
      if (!pod || !!pods.length) App.exitWebFullScreen();
    }
  };
  const { EMPTY: EMPTY$1, ONE_SEC, SHOW_TOAST_TIME, SHOW_TOAST_POSITION } = constants;
  const App = {
    init() {
      this.setupVisibleListener();
      this.setupKeydownListener();
      this.setupMutationObserver();
      this.setupUrlChangeListener();
      this.setupMouseMoveListener();
    },
    isLive() {
      return webSite.isLivePage() || this.videoInfo?.isLive;
    },
    normalWebsite() {
      return !this.videoInfo;
    },
    getVideo: () => Tools.query("video:not([loop]):not([src=''])") || Tools.query("video:not([loop])"),
    getElement: () => Tools.query(selectorConfig[location.host]?.webfull),
    getVideoIframe() {
      if (!this.videoInfo?.frameSrc) return null;
      const url = new URL(this.videoInfo.frameSrc);
      const src = decodeURI(url.pathname + url.search);
      return Tools.query(`iframe[src*="${src}"]`);
    },
    setupVisibleListener() {
      window.addEventListener("visibilitychange", () => {
        window.top.focus();
        if (this.normalWebsite()) return;
        const video = this.isLive() ? this.getVideo() : this.video;
        if (!video || video?.isEnded || !Tools.isVisible(video)) return;
        document.hidden ? video?.pause() : video?.play();
      });
    },
    setupUrlChangeListener() {
      const _wr = (method) => {
        const original = history[method];
        history[method] = function() {
          original.apply(history, arguments);
          window.dispatchEvent(new Event(method));
        };
      };
      const handler = Tools.debounce(() => this.setupMutationObserver());
      ["popstate", "pushState", "replaceState"].forEach((t) => _wr(t) & window.addEventListener(t, handler));
    },
    setupMutationObserver() {
      const observer = Tools.createObserver(document.body, () => {
        const video = this.getVideo();
        this.element = this.getElement();
        if (video?.play && !!video.offsetWidth) this.setupVideoListener();
        if (!webSite.inMatches() && this.topWinInfo) return observer.disconnect();
        if (!video?.play || !this.element || !this.webFullScreen(video)) return;
        observer.disconnect();
        this.biliLiveExtras();
        this.webSiteLoginObserver();
      });
      setTimeout(() => observer.disconnect(), ONE_SEC * 10);
    },
    video: null,
    videoBoundListeners: [],
    setupVideoListener() {
      const video = this.getVideo();
      this.addVideoEvtListener(video);
      this.healthCurrentVideo();
    },
    addVideoEvtListener(video) {
      this.video = video;
      this.setVideoInfo(video);
      this.removeVideoEvtListener();
      for (const type of Object.keys(VideoListenerHandler)) {
        const handler = VideoListenerHandler[type];
        this.video.addEventListener(type, handler);
        this.videoBoundListeners.push([this.video, type, handler]);
      }
    },
    removeVideoEvtListener() {
      this.videoBoundListeners.forEach((listener) => {
        const [target, type, handler] = listener;
        target.removeEventListener(type, handler);
      });
      this.videoBoundListeners = [];
    },
    healthCurrentVideo() {
      if (this.healthID) clearInterval(this.healthID);
      this.healthID = setInterval(() => this.getPlayingVideo(), ONE_SEC);
    },
    getPlayingVideo() {
      const videos = Tools.querys("video");
      for (const video of videos) {
        const isWiderThanWindow = video.offsetWidth > window.innerWidth;
        if (this.video === video || video.paused || !Tools.validDuration(video) || isWiderThanWindow) continue;
        return this.addVideoEvtListener(video);
      }
    },
    setVideoInfo(video) {
      const videoInfo = { ...Tools.getElementCenterPoint(video), isLive: video.duration === Infinity };
      this.setParentVideoInfo(videoInfo);
    },
    setParentVideoInfo(videoInfo) {
      this.videoInfo = videoInfo;
      if (!Tools.isTopWin()) videoInfo.frameSrc = location.href;
      if (!Tools.isTopWin()) return Tools.postMessage(window.parent, { videoInfo });
      this.setupPickerEpisodeListener();
      this.setupScriptMenuCommand();
      this.sendTopWinInfo();
    },
    changeVideoInfo(video) {
      if (!this.videoInfo) return;
      const isLive = video.duration === Infinity;
      if (this.videoInfo.isLive === isLive) return;
      this.videoInfo.isLive = isLive;
      this.setParentVideoInfo(this.videoInfo);
    },
    sendTopWinInfo() {
      const topWinInfo = this.topWinInfo = { innerWidth, host: location.host };
      Tools.postMsgToFrames({ topWinInfo });
    },
    setupMouseMoveListener() {
      if (this.isSetupMouseMoveListener) return;
      const delay = ONE_SEC * 2;
      this.isSetupMouseMoveListener = true;
      let timer = setTimeout(() => this.showOrHideCursor(), delay);
      document.addEventListener("mousemove", (event) => {
        if (!event.isTrusted) return;
        clearTimeout(timer);
        const target = event.target;
        this.showOrHideCursor(false);
        timer = setTimeout(() => this.showOrHideCursor(), delay);
        if (this.video === target || !(target instanceof HTMLVideoElement)) return;
        this.addVideoEvtListener(target);
      });
    },
    showOrHideCursor(isHide = true) {
      if (!this.videoInfo) return;
      const videoWrap = this.getVideoLocation();
      const elements = Array.from([...videoWrap.children, videoWrap, videoWrap.parentElement]);
      elements.forEach((ele) => {
        if (isHide) ele?.dispatchEvent(new MouseEvent("mouseleave"));
        isHide ? ele.style.cursor = "none" : ele.style.cursor = EMPTY$1;
      });
    },
    showToast(content, duration = SHOW_TOAST_TIME) {
      const el = document.createElement("div");
      if (content instanceof HTMLElement) el.appendChild(content);
      if (Object.is(typeof content, typeof EMPTY$1)) el.textContent = content;
      el.setAttribute("class", "showToast");
      el.setAttribute("style", SHOW_TOAST_POSITION);
      const videoParent = this.video?.parentElement;
      const videoContainer = this.getVideoContainer();
      const target = this.video === videoContainer ? videoParent : videoContainer;
      if (webSite.isDouyu()) douyu.addStyle(target);
      Tools.query(".showToast", target)?.remove();
      target?.appendChild(el);
      setTimeout(() => {
        el.style.opacity = 0;
        setTimeout(() => el.remove(), ONE_SEC / 3);
      }, duration);
    }
  };
  const setStorage = function(value) {
    _GM_setValue(this.name, value);
  };
  const getStorage = function(defaultValue) {
    return _GM_getValue(this.name, defaultValue);
  };
  const storage = {
    CACHED_PLAY_RATE: Object.freeze({
      name: "FENY_SCRIPTS_V_PLAYBACK_RATE",
      set(value) {
        localStorage.setItem(this.name, value);
      },
      get() {
        return localStorage.getItem(this.name);
      }
    }),
    PLAY_RATE_STEP: Object.freeze({
      name: "PLAY_RATE_STEP",
      set: setStorage,
      get() {
        return Number.parseFloat(getStorage.call(this, 0.25));
      }
    }),
    CLOSE_PLAY_RATE: Object.freeze({
      name: "CLOSE_PLAY_RATE",
      set: setStorage,
      get() {
        return getStorage.call(this, false);
      }
    }),
    VIDEO_FASTFORWARD_DURATION: Object.freeze({
      name: "VIDEO_FASTFORWARD_DURATION",
      set: setStorage,
      get() {
        return Number.parseInt(getStorage.call(this, 30));
      }
    }),
    VIDEO_TIME_STEP: Object.freeze({
      name: "VIDEO_TIME_STEP",
      set: setStorage,
      get() {
        return Number.parseInt(getStorage.call(this, 5));
      }
    }),
    CLOSE_AUTO_WEB_FULL_SCREEN: Object.freeze({
      name: "CLOSE_AUTO_WEB_FULL_SCREEN",
      set: setStorage,
      get() {
        return getStorage.call(this, false);
      }
    }),
    OVERRIDE_KEYBOARD: Object.freeze({
      name: "OVERRIDE_KEYBOARD",
      set: setStorage,
      get() {
        return getStorage.call(this, false);
      }
    }),
    CLOSE_OTHER_WEBSITES_AUTO: Object.freeze({
      name: "CLOSE_OTHER_WEBSITES_AUTO_",
      set(key, value) {
        _GM_setValue(this.name + key, value);
      },
      get(key) {
        return _GM_getValue(this.name + key, true);
      }
    }),
    CURRENT_EPISODE_CHAIN: Object.freeze({
      name: "CURRENT_EPISODE_CHAIN_",
      set(key, value) {
        _GM_setValue(this.name + key, value);
      },
      get(key) {
        return _GM_getValue(this.name + key);
      },
      delete(key) {
        _GM_deleteValue(this.name + key);
      }
    }),
    ALL_EPISODE_CHAIN: Object.freeze({
      name: "ALL_EPISODE_CHAIN_",
      set(key, value) {
        _GM_setValue(this.name + key, value);
      },
      get(key) {
        return _GM_getValue(this.name + key);
      },
      delete(key) {
        _GM_deleteValue(this.name + key);
      }
    })
  };
  const eventCode = Object.freeze({
    KeyA: "KeyA",
    KeyD: "KeyD",
    KeyF: "KeyF",
    KeyN: "KeyN",
    KeyP: "KeyP",
    KeyS: "KeyS",
    KeyZ: "KeyZ",
    Space: "Space",
    ArrowLeft: "ArrowLeft",
    ArrowRight: "ArrowRight",
    NumpadAdd: "NumpadAdd",
    NumpadSubtract: "NumpadSubtract"
  });
  const { VIDEO_TIME_STEP: VIDEO_TIME_STEP$1, VIDEO_FASTFORWARD_DURATION: VIDEO_FASTFORWARD_DURATION$1 } = storage;
  const { EMPTY, SYMBOL: SYMBOL$1, MSG_SOURCE } = constants;
  const KeydownHandler = {
    preventDefault(event) {
      const overrideKey = [eventCode.Space, eventCode.ArrowLeft, eventCode.ArrowRight];
      const isOverrideKey = this.isOverrideKeyboard() && overrideKey.includes(event.code);
      const isNumberKey = Tools.isNumber(event.key) && !this.isClosedPlayRate();
      if (!isNumberKey && !isOverrideKey) return;
      Tools.preventDefault(event);
    },
    setupKeydownListener() {
      window.addEventListener("keyup", (event) => this.preventDefault(event), true);
      window.addEventListener("keydown", (event) => this.keydownHandler.call(this, event), true);
      window.addEventListener("message", (event) => {
        const { data } = event;
        if (!data?.source || !data.source.includes(MSG_SOURCE)) return;
        if (data?.videoInfo) return this.setParentVideoInfo(data.videoInfo);
        if (data?.defaultPlaybackRate) this.defaultPlaybackRate();
        if (data?.topWinInfo) this.topWinInfo = data.topWinInfo;
        this.processEvent(data);
      });
    },
    keydownHandler(event) {
      if (this.normalWebsite()) return;
      let key = event.key.toUpperCase();
      const { code, target, shiftKey } = event;
      if (["INPUT", "TEXTAREA", "DEMAND-SEARCH-BOX"].includes(target.tagName)) return;
      if (!Object.keys(eventCode).includes(code) && !Tools.isNumber(key)) return;
      this.preventDefault(event);
      if (eventCode.Space === code) key = eventCode.Space.toUpperCase();
      if (shiftKey && eventCode.NumpadAdd === code) key = SYMBOL$1.MULTIPLY;
      if (shiftKey && eventCode.NumpadSubtract === code) key = SYMBOL$1.DIVIDE;
      if (!Tools.isTopWin() && [eventCode.KeyP, eventCode.KeyN].includes(code)) {
        return Tools.postMessage(window.top, { key });
      }
      this.processEvent({ key });
    },
    processEvent(data) {
      if (!this.video) Tools.postMsgToFrames(data);
      if (data?.key) this.execHotKeyActions(data.key);
    },
    execHotKeyActions(key) {
      if (this.normalWebsite()) return;
      const keyMapping = this.getKeyMapping();
      if (keyMapping[key]) return keyMapping[key]();
      if (Tools.isNumber(key)) this.setPlaybackRate(key);
    },
    getKeyMapping() {
      return {
        Z: () => this.defaultPlaybackRate(),
        A: () => this.adjustPlaybackRate(SYMBOL$1.ADD),
        S: () => this.adjustPlaybackRate(SYMBOL$1.SUBTRACT),
        [SYMBOL$1.ADD]: () => this.adjustPlaybackRate(SYMBOL$1.ADD),
        [SYMBOL$1.DIVIDE]: () => this.adjustPlaybackRate(SYMBOL$1.DIVIDE),
        [SYMBOL$1.SUBTRACT]: () => this.adjustPlaybackRate(SYMBOL$1.SUBTRACT),
        [SYMBOL$1.MULTIPLY]: () => this.adjustPlaybackRate(SYMBOL$1.MULTIPLY),
        N: () => webSite.inMatches() ? this.triggerIconElement("next") : this.switchNextEpisode(),
        ARROWLEFT: () => this.isOverrideKeyboard() ? this.adjustVideoTime(SYMBOL$1.SUBTRACT) : null,
        ARROWRIGHT: () => this.isOverrideKeyboard() ? this.adjustVideoTime() : null,
        0: () => this.adjustVideoTime(VIDEO_FASTFORWARD_DURATION$1.get()),
        P: () => {
          if (!webSite.inMatches()) return this.enhance();
          webSite.isBiliLive() ? this.biliLiveWebFullScreen() : this.triggerIconElement("webfull");
        },
        SPACE: () => {
          if (!this.video || !this.isOverrideKeyboard()) return;
          if (webSite.isDouyu()) return this.video.paused ? douyu.play() : douyu.pause();
          this.video.paused ? this.video.play() : this.video.pause();
        },
        F: () => this.triggerIconElement("full", 0),
        D: () => this.triggerIconElement("danmaku", 3)
      };
    },
    triggerIconElement(name, index) {
      if (!webSite.inMatches()) return;
      if (webSite.isBiliLive()) return this.getBiliLiveIcons()?.[index]?.click();
      Tools.query(selectorConfig[location.host]?.[name])?.click();
    },
    adjustVideoTime(second = VIDEO_TIME_STEP$1.get(), _symbol) {
      if (!this.video || !Tools.validDuration(this.video)) return;
      if (_symbol && ![SYMBOL$1.ADD, SYMBOL$1.SUBTRACT].includes(_symbol)) return;
      if (Object.is(typeof second, typeof EMPTY) && !_symbol) {
        _symbol = second;
        second = VIDEO_TIME_STEP$1.get();
      }
      second = Object.is(SYMBOL$1.SUBTRACT, _symbol) ? -second : second;
      const currentTime = this.video.currentTime + second;
      this.video.currentTime = Math.max(0, currentTime);
    }
  };
  const {
    PLAY_RATE_STEP: PLAY_RATE_STEP$1,
    CLOSE_PLAY_RATE,
    VIDEO_TIME_STEP,
    OVERRIDE_KEYBOARD,
    ALL_EPISODE_CHAIN: ALL_EPISODE_CHAIN$2,
    CURRENT_EPISODE_CHAIN: CURRENT_EPISODE_CHAIN$1,
    CLOSE_OTHER_WEBSITES_AUTO,
    CLOSE_AUTO_WEB_FULL_SCREEN,
    VIDEO_FASTFORWARD_DURATION
  } = storage;
  const MenuCommandHandler = {
    isClosedPlayRate: () => CLOSE_PLAY_RATE.get(),
    isOverrideKeyboard: () => OVERRIDE_KEYBOARD.get(),
    isClosedAuto: () => CLOSE_AUTO_WEB_FULL_SCREEN.get(),
    isClosedOtherWebsiteAuto() {
      const host = Tools.isTopWin() ? location.host : this.topWinInfo.host;
      return CLOSE_OTHER_WEBSITES_AUTO.get(host);
    },
    setupScriptMenuCommand() {
      if (!Tools.isTopWin() || this.hasRegisterMenu) return;
      this.registerMenuCommand();
      this.setupCommandChangeListener();
      this.hasRegisterMenu = true;
    },
    registerMenuCommand() {
      this.registerClosePlayRate();
      this.registerPlayRateCommand();
      this.registerVideoTimeCommand();
      this.registerFastforwardCommand();
      this.registerCloseAutoFullCommand();
      this.registerOverrideKeyboardCommand();
      this.registerCloseAutoExperimentCommand();
      this.registerDeletePickerEpisodeCommand();
    },
    setupCommandChangeListener() {
      if (this.hasCommandListener) return;
      const handler = () => this.registerMenuCommand();
      [
        CLOSE_PLAY_RATE.name,
        OVERRIDE_KEYBOARD.name,
        CLOSE_AUTO_WEB_FULL_SCREEN.name,
        CURRENT_EPISODE_CHAIN$1.name + location.host,
        CLOSE_OTHER_WEBSITES_AUTO.name + location.host
      ].forEach((key) => _GM_addValueChangeListener(key, handler));
      this.hasCommandListener = true;
    },
    registerClosePlayRate() {
      const isClose = this.isClosedPlayRate();
      const title = isClose ? "启用倍速功能" : "禁用倍速功能";
      this.unregisterMenuCommand(this.close_play_rate_command_id);
      if (this.isLive()) return;
      this.close_play_rate_command_id = _GM_registerMenuCommand(title, () => {
        if (!isClose) Tools.postMessage(window, { defaultPlaybackRate: true });
        CLOSE_PLAY_RATE.set(!isClose);
      });
    },
    registerPlayRateCommand() {
      const title = "设置倍速步进";
      this.unregisterMenuCommand(this.play_rate_command_id);
      if (this.isLive() || this.isClosedPlayRate()) return;
      this.play_rate_command_id = _GM_registerMenuCommand(title, () => {
        const input = prompt(title, PLAY_RATE_STEP$1.get());
        if (!isNaN(input) && Number.parseFloat(input)) PLAY_RATE_STEP$1.set(input);
      });
    },
    registerVideoTimeCommand() {
      this.unregisterMenuCommand(this.video_time_command_id);
      if (this.isLive() || !this.isOverrideKeyboard()) return;
      const title = "设置快进/退秒数";
      this.video_time_command_id = _GM_registerMenuCommand(title, () => {
        const input = prompt(title, VIDEO_TIME_STEP.get());
        if (!isNaN(input) && Number.parseInt(input)) VIDEO_TIME_STEP.set(input);
      });
    },
    registerFastforwardCommand() {
      this.unregisterMenuCommand(this.fastforward_command_id);
      if (this.isLive()) return;
      const title = "设置零键快进秒数";
      this.fastforward_command_id = _GM_registerMenuCommand(title, () => {
        const input = prompt(title, VIDEO_FASTFORWARD_DURATION.get());
        if (!isNaN(input) && Number.parseInt(input)) VIDEO_FASTFORWARD_DURATION.set(input);
      });
    },
    registerCloseAutoFullCommand() {
      if (!webSite.inMatches()) return;
      const isClose = this.isClosedAuto();
      const title = isClose ? "启用自动网页全屏" : "禁用自动网页全屏";
      this.unregisterMenuCommand(this.close_auto_command_id);
      this.close_auto_command_id = _GM_registerMenuCommand(title, () => CLOSE_AUTO_WEB_FULL_SCREEN.set(!isClose));
    },
    registerOverrideKeyboardCommand() {
      const isOverride = this.isOverrideKeyboard();
      const title = isOverride ? "禁用 空格 ◀▶ 键控制" : "启用 空格 ◀▶ 键控制";
      this.unregisterMenuCommand(this.override_keyboard_command_id);
      this.override_keyboard_command_id = _GM_registerMenuCommand(title, () => OVERRIDE_KEYBOARD.set(!isOverride));
    },
    registerCloseAutoExperimentCommand() {
      if (webSite.inMatches()) return;
      const videos = Tools.querys("video").filter((video) => !isNaN(video));
      if (videos.length > 1) return;
      const isClose = this.isClosedOtherWebsiteAuto();
      const title = isClose ? "此站点启用自动网页全屏" : "此站点禁用自动网页全屏";
      this.unregisterMenuCommand(this.close_experiment_command_id);
      this.close_experiment_command_id = _GM_registerMenuCommand(title, () => {
        CLOSE_OTHER_WEBSITES_AUTO.set(location.host, !isClose);
      });
    },
    registerDeletePickerEpisodeCommand() {
      const title = "删除此站点的剧集选择器";
      this.unregisterMenuCommand(this.del_piker_episode_command_id);
      if (webSite.inMatches() || !CURRENT_EPISODE_CHAIN$1.get(location.host)) return;
      this.del_piker_episode_command_id = _GM_registerMenuCommand(title, () => {
        CURRENT_EPISODE_CHAIN$1.delete(location.host);
        ALL_EPISODE_CHAIN$2.delete(location.host);
      });
    },
    unregisterMenuCommand(command_id) {
      _GM_unregisterMenuCommand(command_id);
      command_id = null;
    }
  };
  const WebSiteLoginHandler = {
    webSiteLoginObserver() {
      this.handleIqyLogin();
      this.handleBiliLogin();
      this.handleTencentLogin();
    },
    loginObserver(target, matches2, clickTarget) {
      return Tools.createObserver(target, (mutations, observer) => {
        mutations.forEach((mutation) => {
          if (mutation.addedNodes.length === 0) return;
          mutation.addedNodes.forEach((node) => {
            if (node.nodeType !== Node.ELEMENT_NODE) return;
            if (!node.matches(matches2)) return;
            Tools.query(clickTarget)?.click();
            observer.disconnect();
          });
        });
      });
    },
    handleTencentLogin() {
      if (!webSite.isTencent()) return;
      const selector = ".main-login-wnd-module_close-button__mt9WU";
      this.loginObserver("#login_win", selector, selector);
    },
    handleIqyLogin() {
      if (!webSite.isIqiyi()) return;
      const selector = ".simple-buttons_close_btn__6N7HD";
      this.loginObserver("#qy_pca_login_root", selector, selector);
      Tools.createObserver(".cd-time", () => {
        const selector2 = ":is([id*='mask-layer'], #modal-vip-cashier-scope)";
        Tools.querys(selector2).forEach((el) => el.remove());
        Tools.query(".simple-buttons_close_btn__6N7HD")?.click();
        const adTime = Tools.query(".public-time");
        if (adTime.style.display === "none") return;
        if (this.video.currentTime !== this.video.duration) return;
        Tools.querys("*:not(.public-vip)", adTime).forEach((el) => el.click());
      });
    },
    handleBiliLogin() {
      if (!webSite.isBili()) return;
      if (document.cookie.includes("DedeUserID")) return _unsafeWindow.player?.requestQuality(80);
      setTimeout(() => {
        _unsafeWindow.__BiliUser__.isLogin = true;
        _unsafeWindow.__BiliUser__.cache.data.isLogin = true;
        _unsafeWindow.__BiliUser__.cache.data.mid = Date.now();
      }, constants.ONE_SEC * 3);
    }
  };
  const WebFullScreenHandler = {
    webFullScreen(video) {
      const width = video?.offsetWidth;
      if (!width) return false;
      if (this.isClosedAuto() || width >= window.innerWidth) return true;
      if (!webSite.isBiliLive()) return Tools.triggerClick(this.element);
      return this.biliLiveWebFullScreen();
    },
    biliLiveWebFullScreen() {
      const control = this.getBiliLiveIcons();
      if (!control.length) return false;
      Tools.scrollTop(70);
      const el = Tools.query(":is(.lite-room, #player-ctnr)", _unsafeWindow.top.document);
      if (el) Tools.scrollTop(Tools.getElementRect(el)?.top || 0);
      return Tools.triggerClick(control[1]);
    },
    exitWebFullScreen() {
      if (window.innerWidth === this.video.offsetWidth) this.getElement()?.click();
      const cancelButton = Tools.query(".bpx-player-ending-related-item-cancel");
      if (cancelButton) setTimeout(() => cancelButton.click(), 100);
    },
    biliLiveExtras() {
      _unsafeWindow.top?.livePlayer?.volume(100);
      _unsafeWindow.top?.livePlayer?.switchQuality("10000");
      localStorage.setItem("FULLSCREEN-GIFT-PANEL-SHOW", 0);
      document.body.classList.add("hide-asida-area", "hide-aside-area");
    },
    getBiliLiveIcons() {
      const video = this.getVideo();
      if (!video) return [];
      Tools.triggerMousemove(video);
      return Tools.querys("#web-player-controller-wrap-el .right-area .icon");
    },
    experimentWebFullScreen(video) {
      if (webSite.inMatches() || video.isWebFullScreen || !this.topWinInfo || this.isClosedOtherWebsiteAuto()) return;
      if (video.offsetWidth === this.topWinInfo.innerWidth) return video.isWebFullScreen = true;
      Tools.postMessage(window.top, { key: "P" });
      video.isWebFullScreen = true;
    }
  };
  const { ALL_EPISODE_CHAIN: ALL_EPISODE_CHAIN$1 } = storage;
  const SwitchEpisodeHandler = {
    switchPrevEpisode: function() {
      this.handleEpisodeChange(this.getPrevEpisode);
    },
    switchNextEpisode: function() {
      this.handleEpisodeChange(this.getNextEpisode);
    },
    handleEpisodeChange(getTargetEpisode) {
      if (!Tools.isTopWin()) return;
      let currEpisode = this.getCurrentEpisode();
      if (!currEpisode) currEpisode = this.getCurrentEpisodeFromAllLink();
      const targetEpisode = getTargetEpisode.call(this, currEpisode);
      this.jumpToEpisodeNumber(targetEpisode);
    },
    getCurrentEpisode() {
      const ele = ALL_EPISODE_CHAIN$1.get(location.host) ? this.getCurrentEpisodeForChain() : this.getCurrentEpisodeLinkElement();
      return this.getEpisodeContainer(ele);
    },
    getCurrentEpisodeLinkElement() {
      const href = location.href;
      const path = location.pathname;
      const lastPath = path.substring(path.lastIndexOf("/") + 1);
      const links = Tools.querys(`:is(a[href*="${path}"], a[href*="${lastPath}"])`);
      if (links.length == 1) return links.shift();
      const filter = [
        "h1",
        "header",
        "[id*='history']",
        "[class*='lishi']",
        "[class*='record']",
        "[class*='history']",
        "[class*='tab-item']",
        "[class*='play-channel']"
      ];
      return links.find((link) => {
        const linkUrl = new URL(link.href);
        if (Tools.closest(link, `:is(${filter})`, 5)) return false;
        if (!Object.is(href, link.href) && !Object.is(path, linkUrl.pathname)) return false;
        return !!this.getEpisodeNumber(link);
      });
    },
    getEpisodeNumber(element) {
      return Tools.extractNumbers(element?.innerText).shift();
    },
    getPrevEpisode(element) {
      return this.getEpisodeNumberContainer(element, true);
    },
    getNextEpisode(element) {
      return this.getEpisodeNumberContainer(element);
    },
    getEpisodeNumberContainer(element, isPrev = false) {
      if (!element) return;
      const currNumber = this.getEpisodeNumber(element);
      const episodes = this.getAllEpisodeElement(element);
      if (episodes.length <= 1) return null;
      const numbers = episodes.map(this.getEpisodeNumber);
      const index = episodes.indexOf(element);
      const prev = episodes[index - 1];
      const next = episodes[index + 1];
      const { leftSmall, rightLarge } = this.compareLeftRight(numbers, currNumber, index);
      if (leftSmall || rightLarge) return isPrev ? prev : next;
      return isPrev ? next : prev;
    },
    compareLeftRight(numbers, compareNumber, index) {
      const leftSmall = numbers.findIndex((val, i) => i < index && val < compareNumber) > -1;
      const rightLarge = numbers.findIndex((val, i) => i > index && val > compareNumber) > -1;
      return { leftSmall, rightLarge };
    },
    getAllEpisodeElement(element) {
      const eleName = element.tagName;
      const eleClass = Array.from(element.classList);
      const sibling = Tools.findSiblingInParent(element, eleName);
      const children = Array.from(sibling?.parentElement.children);
      return children.filter((ele) => {
        const currClass = Array.from(ele.classList);
        const haveSomeClass = eleClass.some((value) => currClass.includes(value));
        if (!!currClass.length && !haveSomeClass) return false;
        return ele.tagName === eleName;
      });
    },
    jumpToEpisodeNumber(element) {
      if (!element) return;
      if (element instanceof HTMLAnchorElement) return element.click();
      const stack = [element];
      while (stack.length > 0) {
        const current = stack.pop();
        if (!(current instanceof HTMLElement) || !this.getEpisodeNumber(current)) continue;
        if (current instanceof HTMLAnchorElement || current instanceof HTMLButtonElement) return current.click();
        current.click();
        const children = Array.from(current.children).reverse();
        for (const child of children) {
          stack.push(child);
        }
      }
    },
    getEpisodeContainer(element) {
      while (element) {
        const tagName = element.tagName;
        const parentEle = element.parentElement;
        const haveSib = Tools.haveSiblings(element);
        const hasEqualsTag = Tools.querys(tagName, parentEle).find((el) => el !== element);
        if (haveSib && hasEqualsTag) return element;
        element = parentEle;
      }
      return element;
    },
    getCurrentEpisodeFromAllLink() {
      const path = location.pathname;
      const lastPath = path.substring(path.lastIndexOf("/") + 1);
      if (lastPath === constants.EMPTY) return null;
      const currNumber = [...Tools.extractNumbers(lastPath)].join("");
      for (const link of Tools.querys("a")) {
        const attrs = link.attributes;
        for (const attr of attrs) {
          const attrNumbers = [...Tools.extractNumbers(attr.value)].join("");
          if (attrNumbers === currNumber) return link;
        }
      }
    }
  };
  cssLoader("sweetalert2");
  const { ALL_EPISODE_CHAIN, CURRENT_EPISODE_CHAIN } = storage;
  const PickerEpisodeHandler = {
    setupPickerEpisodeListener() {
      if (webSite.inMatches()) return;
      document.body.addEventListener(
        "click",
        (event) => {
          if (!event.ctrlKey || !event.altKey || !event.isTrusted) return;
          if (!Tools.isTopWin()) return Tools.notyf("此页面不能抓取 (•ิ_•ิ)?", true);
          Tools.preventDefault(event);
          const hasPickerAllEpisode = ALL_EPISODE_CHAIN.get(location.host);
          const hasPickerCurrEpisode = CURRENT_EPISODE_CHAIN.get(location.host);
          if (hasPickerCurrEpisode && hasPickerAllEpisode) {
            return Tools.notyf("已提取过剧集元素 ( ̄ー ̄)", true);
          }
          const target = event.target;
          const number = this.getEpisodeNumber(target);
          if (!number) return Tools.notyf("点击位置无数字 (•ิ_•ิ)?", true);
          !hasPickerCurrEpisode ? this.setCurrentEpisodeChain(target) : this.setAllEpisodeChain(target);
        },
        true
      );
    },
    setCurrentEpisodeChain(element) {
      if (CURRENT_EPISODE_CHAIN.get(location.host)) return;
      this.pickerEpisodeDialog(element, {
        validBtnCallback(value) {
          try {
            const number = this.getEpisodeNumber(Tools.query(value));
            !!number ? Tools.notyf(`当前集数:${number}`) : Tools.notyf("获取集数失败 〒▽〒", true);
          } catch (e) {
            Tools.notyf("获取集数失败 〒▽〒", true);
            console.error(e);
          }
        },
        confirmCallback(value) {
          CURRENT_EPISODE_CHAIN.set(location.host, value);
          Tools.notyf("继续提取元素 \(>0<)/");
        }
      });
    },
    setAllEpisodeChain(element) {
      if (ALL_EPISODE_CHAIN.get(location.host)) return;
      this.pickerEpisodeDialog(element, {
        validBtnCallback(value) {
          try {
            const container = this.getEpisodeContainer(Tools.query(value));
            if (!container) return Tools.notyf("获取集数失败 〒▽〒", true);
            const allEpisode = this.getAllEpisodeElement(container);
            const numbers = allEpisode.map(this.getEpisodeNumber);
            const numJoin = numbers.join(" ");
            !!numbers.length ? Tools.notyf(`所有集数:${numJoin}`) : Tools.notyf("获取集数失败 〒▽〒", true);
          } catch (e) {
            Tools.notyf("获取集数失败 〒▽〒", true);
            console.error(e);
          }
        },
        confirmCallback(value) {
          ALL_EPISODE_CHAIN.set(location.host, value);
          Tools.notyf("操作完成 []~( ̄▽ ̄)~* 干杯");
        }
      });
    },
    getCurrentEpisodeForChain() {
      const currChain = CURRENT_EPISODE_CHAIN.get(location.host);
      if (!currChain) return;
      const currEpisode = Tools.query(currChain);
      const currNumber = this.getEpisodeNumber(currEpisode);
      const chain = ALL_EPISODE_CHAIN.get(location.host);
      const container = this.getEpisodeContainer(Tools.query(chain));
      const episodes = this.getAllEpisodeElement(container);
      return episodes.includes(currEpisode) ? currEpisode : episodes.find((ele) => this.getEpisodeNumber(ele) === currNumber);
    },
    pickerEpisodeDialog(element, { validBtnCallback, confirmCallback }) {
      Swal.fire({
        html: `<h4>验证能正确获取到集数,再确定保存</h4>
      <textarea id="picker-chain" class="swal2-textarea" placeholder="请输入元素选择器"></textarea>
      <p>编辑选择器确保能正确获取到集数</p>`,
        customClass: { popup: "monkey-web-fullscreen" },
        title: "抓取剧集元素选择器",
        confirmButtonText: "保存",
        denyButtonText: "验证",
        showCloseButton: true,
        showDenyButton: true,
        reverseButtons: true,
        focusDeny: true,
        preDeny: () => {
          const value = Tools.query("#picker-chain").value.trim();
          if (!value) return Tools.notyf("元素选择器不能为空!", true);
          validBtnCallback.call(this, value);
          return false;
        },
        preConfirm: () => {
          const value = Tools.query("#picker-chain").value.trim();
          if (value) return value;
          Tools.notyf("元素选择器不能为空!", true);
          return false;
        },
        didOpen: () => {
          const textarea = Tools.query("#picker-chain");
          textarea.value = Tools.getParentChain(element);
        }
      }).then((result) => {
        if (!result.isConfirmed) return;
        confirmCallback.call(this, result.value);
      });
    }
  };
  const ScriptsEnhanceHandler = {
    enhance() {
      if (!this.videoInfo) return;
      const ele = this.getVideoLocation();
      Tools.triggerHoverEvent(ele);
      Tools.triggerEscapeEvent();
      this.backupTrigger();
    },
    backupTrigger() {
      if (!this.video || this.videoInfo.frameSrc) return;
      let oldWidth = this.video.oldWidth;
      let newWidth = this.video.offsetWidth;
      if (!Object.is(oldWidth, newWidth)) return;
      Tools.query("#playerControlBtn")?.click();
      if (!Object.is(newWidth, this.video.offsetWidth)) return;
      Tools.query("#playerControlBtn")?.click();
    },
    getVideoLocation() {
      if (this.video) return this.getVideoContainer();
      const iframe = this.getVideoIframe();
      if (iframe) return iframe;
      const { centerX, centerY } = this.videoInfo;
      const iframes = Tools.getFrames();
      for (const element of iframes) {
        if (!Tools.isVisible(element)) continue;
        if (Tools.isPointInElementRect(centerX, centerY, element)) return element;
      }
    },
    getVideoContainer() {
      const video = this.video;
      video.oldWidth = video.offsetWidth;
      const parentEle = video?.parentElement;
      const control = this.getVideoControls(video);
      const player = this.getVideoPlayer(parentEle);
      const videoContainer = player || control?.parentElement;
      if (!videoContainer) return video;
      if (this.videoInfo.frameSrc) return videoContainer;
      const videoWidth = video.offsetWidth;
      const wrapWidth = videoContainer.offsetWidth;
      return wrapWidth !== videoWidth ? video : videoContainer;
    },
    getVideoPlayer(target) {
      const selector = [
        ".video-js",
        '[class*="wrap"]',
        '[class*="video"]',
        '[class*="Player"]',
        '[class*="player"]',
        '[aria-label="视频播放器"]',
        '[aria-label="Video Player"]'
      ];
      return Tools.closest(target, `:is(${selector})`);
    },
    getVideoControls(element) {
      return Tools.findSiblingInParent(element, ['[class*="bar"]', '[class*="Control"]', '[class*="control"]']);
    }
  };
  const { PLAY_RATE_STEP, CACHED_PLAY_RATE } = storage;
  const { SYMBOL, DEF_PLAY_RATE, MAX_PLAY_RATE } = constants;
  const strategy = {
    [SYMBOL.MULTIPLY]: (playRate) => playRate * 2,
    [SYMBOL.DIVIDE]: (playRate) => playRate / 2,
    [SYMBOL.ADD]: (playRate) => playRate + PLAY_RATE_STEP.get(),
    [SYMBOL.SUBTRACT]: (playRate) => playRate - PLAY_RATE_STEP.get()
  };
  const VideoPlaybackRateHandler = {
    checkUsable() {
      if (!this.video) return false;
      if (webSite.isLivePage()) return false;
      if (this.isClosedPlayRate()) return false;
      if (!Tools.validDuration(this.video)) return false;
      return true;
    },
    toFixed(playRate) {
      playRate = Number.parseFloat(playRate);
      return playRate.toFixed(2).replace(/\.?0+$/, "");
    },
    setPlaybackRate(playRate, show = true) {
      if (!this.checkUsable()) return;
      this.video.playbackRate = this.toFixed(playRate);
      if (show) this.playbackRateToast();
      this.cachePlaybackRate();
    },
    adjustPlaybackRate(_symbol) {
      if (!this.checkUsable()) return;
      let playRate = this.video.playbackRate;
      playRate = strategy[_symbol](playRate);
      playRate = Math.max(PLAY_RATE_STEP.get(), playRate);
      playRate = Math.min(MAX_PLAY_RATE, playRate);
      this.setPlaybackRate(playRate);
    },
    defaultPlaybackRate() {
      if (!this.video) return;
      this.video.playbackRate = DEF_PLAY_RATE;
      if (this.isClosedPlayRate()) return;
      this.cachePlaybackRate();
      this.showToast("已恢复正常倍速播放");
    },
    currVideoUseCachePlayRate(video) {
      if (this.isClosedPlayRate()) return;
      if (!webSite.isIqiyi()) video.isToastShown = false;
      const playRate = this.getCachePlaybackRate();
      if (video.playbackRate === playRate) return;
      this.setPlaybackRate(playRate, !video.isToastShown);
      video.isToastShown = true;
    },
    cachePlaybackRate() {
      CACHED_PLAY_RATE.set(this.video.playbackRate);
    },
    getCachePlaybackRate: () => Number.parseFloat(CACHED_PLAY_RATE.get() || DEF_PLAY_RATE),
    playbackRateToast() {
      const span = document.createElement("span");
      span.appendChild(document.createTextNode("正在以"));
      const child = span.cloneNode(true);
      child.textContent = `${this.video.playbackRate}x`;
      child.setAttribute("style", "margin:0 3px!important;color:#ff6101!important;");
      span.appendChild(child);
      span.appendChild(document.createTextNode("倍速播放"));
      this.showToast(span);
    }
  };
  const logicHandlers = [
    { handler: KeydownHandler },
    { handler: MenuCommandHandler },
    { handler: WebSiteLoginHandler },
    { handler: WebFullScreenHandler },
    { handler: SwitchEpisodeHandler },
    { handler: PickerEpisodeHandler },
    { handler: ScriptsEnhanceHandler },
    { handler: VideoPlaybackRateHandler }
  ];
  logicHandlers.forEach(({ handler }) => {
    for (const key of Object.keys(handler)) {
      const method = handler[key];
      method instanceof Function ? App[key] = method.bind(App) : App[key] = method;
    }
  });
  App.init();
  _unsafeWindow.MONKEY_WEB_FULLSCREEN = App;

})(notyf, sweetalert2);