Maximize Video Fixed

Maximize all video players. Support picture-in-picture. Maintained fork of Maximize Video by 冻猫.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Greasemonkey lub Violentmonkey.

You will need to install an extension such as Tampermonkey to install this script.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana będzie instalacja rozszerzenia Tampermonkey lub Userscripts.

You will need to install an extension such as Tampermonkey to install this script.

Aby zainstalować ten skrypt, musisz zainstalować rozszerzenie menedżera skryptów użytkownika.

(Mam już menedżera skryptów użytkownika, pozwól mi to zainstalować!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Musisz zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

(Mam już menedżera stylów użytkownika, pozwól mi to zainstalować!)

// ==UserScript==
// @name                Maximize Video Fixed
// @name:zh-CN          视频网页全屏(修复版)
// @namespace           https://github.com/chinnsenn/maximize-video-fork
// @description         Maximize all video players. Support picture-in-picture. Maintained fork of Maximize Video by 冻猫.
// @description:zh-CN   让所有视频网页全屏,开启画中画功能。基于冻猫原版 Maximize Video 的维护分支。
// @author              冻猫
// @homepageURL         https://github.com/chinnsenn/maximize-video-fork
// @supportURL          https://github.com/chinnsenn/maximize-video-fork/issues
// @match               *://*/*
// @exclude             *www.w3school.com.cn*
// @version             1.0.4
// @run-at              document-start
// ==/UserScript==

/**
 * [INPUT]: 依赖 Greasy Fork/Tampermonkey userscript 元数据规范
 * [OUTPUT]: 对外提供 Maximize Video Fixed 的发布元数据、维护主页与更新链接
 * [POS]: src 的元数据契约,被 scripts/build.mjs 置于最终 userscript 文件头部
 * [PROTOCOL]: 变更时更新此头部,然后检查 CLAUDE.md
 */

;(() => {
"use strict"

/**
 * [INPUT]: 依赖 浏览器 window/document 环境
 * [OUTPUT]: 对外提供 gv 运行时状态容器
 * [POS]: src 的全局状态核心,被按钮、事件、全屏控制共同读写
 * [PROTOCOL]: 变更时更新此头部,然后检查 CLAUDE.md
 */

const gv = {
  isFull: false,
  isIframe: false,
  autoCheckCount: 0,
}

/**
 * [INPUT]: 依赖 站点 hostname 与 CSS selector 约定
 * [OUTPUT]: 对外提供 html5Rules 与 generalPlayerRules 播放器识别规则
 * [POS]: src 的规则表,被 handlers 与 buttons 检测流程消费
 * [PROTOCOL]: 变更时更新此头部,然后检查 CLAUDE.md
 */

//Html5规则[播放器最外层],适用于无法自动识别的自适应大小HTML5播放器
const html5Rules = {
  "www.acfun.cn": [".player-container .player"],
  "*bilibili.com": ["#bilibiliPlayer", "#bilibili-player", ".bpx-player-container"],
  "www.douyu.com": ["#js-player-video-case"],
  "www.huya.com": ["#videoContainer"],
  "www.twitch.tv": [".player"],
  "www.youtube.com": ["#movie_player"],
  "www.yy.com": ["#player"],
  "*weibo.com": ['[aria-label="Video Player"]', ".html5-video-live .html5-video"],
  "v.huya.com": ["#video_embed_flash>div"],
}

//通用html5播放器
const generalPlayerRules = [".dplayer", ".video-js", ".jwplayer", "[data-player]"]

/**
 * [INPUT]: 依赖 gv 运行时状态、播放器规则表与后续模块注册
 * [OUTPUT]: 对外提供 runtime 显式运行时注册表
 * [POS]: src 的依赖中枢,让工具、按钮、事件、全屏控制器通过明确名字协作
 * [PROTOCOL]: 变更时更新此头部,然后检查 CLAUDE.md
 */

const runtime = {
  gv,
  rules: {
    html5Rules,
    generalPlayerRules,
  },
  register(name, service) {
    this[name] = service
    return service
  },
}

/**
 * [INPUT]: 依赖 window.top/window.self 与 navigator.language
 * [OUTPUT]: 对外提供 runtime.gv.isIframe 与 runtime.gv.btnText 本地化文案
 * [POS]: src 的环境初始化层,为入口启动前准备跨 iframe 状态与按钮文案
 * [PROTOCOL]: 变更时更新此头部,然后检查 CLAUDE.md
 */

if (window.top !== window.self) {
  runtime.gv.isIframe = true
}

if (navigator.language.toLocaleLowerCase() == "zh-cn") {
  runtime.gv.btnText = {
    max: "网页全屏",
    pip: "画中画",
    tip: "Iframe内视频,请用鼠标点击视频后重试",
  }
} else {
  runtime.gv.btnText = {
    max: "Maximize",
    pip: "PicInPic",
    tip: "Iframe video. Please click on the video and try again",
  }
}

/**
 * [INPUT]: 依赖 DOM、CSSOM 与 runtime.maximize 运行时服务
 * [OUTPUT]: 对外提供 tool DOM/几何/样式/日志工具集
 * [POS]: src 的底层工具层,被所有交互模块复用
 * [PROTOCOL]: 变更时更新此头部,然后检查 CLAUDE.md
 */

const tool = runtime.register("tool", {
  print(log) {
    const now = new Date()
    const year = now.getFullYear()
    const month = (now.getMonth() + 1 < 10 ? "0" : "") + (now.getMonth() + 1)
    const day = (now.getDate() < 10 ? "0" : "") + now.getDate()
    const hour = (now.getHours() < 10 ? "0" : "") + now.getHours()
    const minute = (now.getMinutes() < 10 ? "0" : "") + now.getMinutes()
    const second = (now.getSeconds() < 10 ? "0" : "") + now.getSeconds()
    const timenow = "[" + year + "-" + month + "-" + day + " " + hour + ":" + minute + ":" + second + "]"
    console.log(timenow + "[Maximize Video] > " + log)
  },
  getRect(element) {
    const rect = element.getBoundingClientRect()
    const scroll = tool.getScroll()
    return {
      pageX: rect.left + scroll.left,
      pageY: rect.top + scroll.top,
      screenX: rect.left,
      screenY: rect.top,
      right: rect.right,
      bottom: rect.bottom,
      width: rect.width,
      height: rect.height,
    }
  },
  clamp(value, min, max) {
    return Math.min(Math.max(value, min), max)
  },
  isHalfFullClient(element) {
    const client = tool.getClient()
    const rect = tool.getRect(element)
    if (
      (Math.abs(client.width - element.offsetWidth) < 21 && rect.screenX < 20) ||
      (Math.abs(client.height - element.offsetHeight) < 21 && rect.screenY < 10)
    ) {
      if (
        Math.abs(element.offsetWidth / 2 + rect.screenX - client.width / 2) < 21 &&
        Math.abs(element.offsetHeight / 2 + rect.screenY - client.height / 2) < 21
      ) {
        return true
      } else {
        return false
      }
    } else {
      return false
    }
  },
  isAllFullClient(element) {
    const client = tool.getClient()
    const rect = tool.getRect(element)
    if (
      Math.abs(client.width - element.offsetWidth) < 21 &&
      rect.screenX < 20 &&
      Math.abs(client.height - element.offsetHeight) < 21 &&
      rect.screenY < 10
    ) {
      return true
    } else {
      return false
    }
  },
  getScroll() {
    return {
      left: document.documentElement.scrollLeft || document.body.scrollLeft,
      top: document.documentElement.scrollTop || document.body.scrollTop,
    }
  },
  getClient() {
    return {
      width: document.compatMode == "CSS1Compat" ? document.documentElement.clientWidth : document.body.clientWidth,
      height: document.compatMode == "CSS1Compat" ? document.documentElement.clientHeight : document.body.clientHeight,
    }
  },
  addStyle(css) {
    const style = document.createElement("style")
    style.type = "text/css"
    const node = document.createTextNode(css)
    style.appendChild(node)
    document.head.appendChild(style)
    return style
  },
  matchRule(str, rule) {
    return new RegExp("^" + rule.split("*").join(".*") + "$").test(str)
  },
  setButtonVisible(button, visible) {
    button.style.setProperty("display", visible ? "block" : "none", "important")
    button.style.setProperty("visibility", visible ? "visible" : "hidden", "important")
  },
  styleFloatingButton(button, width) {
    const styles = {
      position: "fixed",
      width: width + "px",
      height: "20px",
      "line-height": "20px",
      margin: "0",
      padding: "0",
      border: "0",
      "border-radius": "0",
      "text-align": "center",
      font: '12px "微软雅黑", sans-serif',
      color: "#fff",
      "background-color": "#27a9d8",
      cursor: "pointer",
      "text-shadow": "none",
      "box-sizing": "border-box",
      transition: "opacity 0.2s ease, background-color 0.2s ease",
    }
    for (const property in styles) button.style.setProperty(property, styles[property], "important")
    button.style.setProperty("z-index", "2147483647", "important")
  },
  createButton(id, hidden = false) {
    const btn = document.createElement("div")
    btn.id = id
    if (id == "playerControlBtn") this.styleFloatingButton(btn, 64)
    if (hidden) this.setButtonVisible(btn, false)
    btn.onclick = () => {
      runtime.maximize.playerControl()
    }
    document.body.appendChild(btn)
    return btn
  },
  async addTip(str) {
    if (!document.getElementById("catTip")) {
      const tip = document.createElement("tbdiv")
      tip.id = "catTip"
      tip.innerHTML = str
      ;(tip.style.cssText =
        'transition: all 0.8s ease-out;background: none repeat scroll 0 0 #27a9d8;color: #FFFFFF;font: 1.1em "微软雅黑";margin-left: -250px;overflow: hidden;padding: 10px;position: fixed;text-align: center;bottom: 100px;z-index: 300;'),
        document.body.appendChild(tip)
      tip.style.right = -tip.offsetWidth - 5 + "px"
      await new Promise((resolve) => {
        tip.style.display = "block"
        setTimeout(() => {
          tip.style.right = "25px"
          resolve("OK")
        }, 300)
      })
      await new Promise((resolve) => {
        setTimeout(() => {
          tip.style.right = -tip.offsetWidth - 5 + "px"
          resolve("OK")
        }, 3500)
      })
      await new Promise((resolve) => {
        setTimeout(() => {
          document.body.removeChild(tip)
          resolve("OK")
        }, 1000)
      })
    }
  },
})

/**
 * [INPUT]: 依赖 document/window 生命周期事件、用户暂停意图事件与 HTMLVideoElement 播放能力
 * [OUTPUT]: 对外提供 antiBlurPause 反失焦暂停控制器
 * [POS]: src 的播放稳定性补丁,入口初始化时最先启用
 * [PROTOCOL]: 变更时更新此头部,然后检查 CLAUDE.md
 */

const antiBlurPause = runtime.register("antiBlurPause", {
  ready: false,
  lastFocusLossAt: 0,
  lastPauseIntentAt: 0,
  focusLossWindowMs: 2000,
  pauseIntentWindowMs: 1200,
  init() {
    if (this.ready) return
    this.ready = true
    this.keepVisible()
    this.trackPauseIntent()
    this.blockFocusEvents()
    this.resumePausedVideos()
  },
  keepVisible() {
    const define = (obj, key, value) => {
      try {
        Object.defineProperty(obj, key, {
          configurable: true,
          get: () => value,
        })
      } catch (_) {}
    }
    define(Document.prototype, "hidden", false)
    define(Document.prototype, "webkitHidden", false)
    define(Document.prototype, "visibilityState", "visible")
    try {
      document.hasFocus = () => true
    } catch (_) {}
  },
  blockFocusEvents() {
    const stop = (event) => {
      this.lastFocusLossAt = Date.now()
      event.stopImmediatePropagation()
    }
    const events = ["visibilitychange", "webkitvisibilitychange", "blur", "pagehide", "freeze"]
    for (const event of events) {
      document.addEventListener(event, stop, true)
      window.addEventListener(event, stop, true)
    }
  },
  trackPauseIntent() {
    const mark = (event) => {
      if (!this.isPauseIntent(event)) return
      this.lastPauseIntentAt = Date.now()
    }
    for (const event of ["pointerdown", "mousedown", "touchstart", "click", "keydown"]) {
      document.addEventListener(event, mark, true)
    }
  },
  isPauseIntent(event) {
    if (!this.hasPlayingVideo()) return false
    if (event.type == "keydown") return this.isMediaKey(event) && !this.isEditableTarget(event.target)
    return this.isMediaControl(event)
  },
  isMediaKey(event) {
    return [" ", "Spacebar", "k", "K", "MediaPlayPause", "MediaPause"].includes(event.key)
  },
  hasPlayingVideo() {
    return [...document.getElementsByTagName("video")].some((video) => !video.paused && !video.ended)
  },
  isEditableTarget(target) {
    return target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target?.isContentEditable
  },
  isMediaControl(event) {
    const path = typeof event.composedPath == "function" ? event.composedPath() : [event.target]
    return path.some((node) => this.isVideoNode(node) || this.looksLikeMediaControl(node))
  },
  isVideoNode(node) {
    return node?.nodeName == "VIDEO"
  },
  looksLikeMediaControl(node) {
    if (!(node instanceof Element)) return false
    const text = `${node.id} ${node.className} ${node.getAttribute("aria-label") || ""} ${node.getAttribute("title") || ""}`
    return /video|player|control|play|pause/i.test(text)
  },
  hasRecentPauseIntent() {
    return Date.now() - this.lastPauseIntentAt <= this.pauseIntentWindowMs
  },
  resumePausedVideos() {
    document.addEventListener(
      "pause",
      (event) => {
        const video = event.target
        if (video?.nodeName != "VIDEO") return
        if (Date.now() - this.lastFocusLossAt > this.focusLossWindowMs) return
        if (video.ended || video.seeking) return
        if (this.hasRecentPauseIntent()) return
        setTimeout(() => {
          if (video.paused && !video.ended) {
            video.play().catch(() => {})
          }
        }, 80)
      },
      true
    )
  },
})

/**
 * [INPUT]: 依赖 runtime.gv、runtime.tool、runtime.rules、runtime.handle 与 runtime.init
 * [OUTPUT]: 对外提供 setButton 按钮创建后的展示、定位与自动探测逻辑
 * [POS]: src 的按钮控制层,连接播放器检测与用户操作入口
 * [PROTOCOL]: 变更时更新此头部,然后检查 CLAUDE.md
 */

const setButton = runtime.register("setButton", {
  init() {
    if (!document.getElementById("playerControlBtn")) {
      runtime.init()
    }
    if (runtime.gv.isIframe && runtime.tool.isHalfFullClient(runtime.gv.player)) {
      window.parent.postMessage("iframeVideo", "*")
      return
    }
    this.show()
  },
  show() {
    if (runtime.gv.isFull || !runtime.gv.player || !runtime.gv.player.isConnected) {
      this.hide()
      return
    }
    runtime.gv.player.removeEventListener("mouseleave", runtime.handle.leavePlayer, false)
    runtime.gv.player.addEventListener("mouseleave", runtime.handle.leavePlayer, false)

    if (!runtime.gv.isFull) {
      document.removeEventListener("scroll", runtime.handle.scrollFix, false)
      document.addEventListener("scroll", runtime.handle.scrollFix, false)
    }
    if (!this.locate()) {
      this.hide()
      return
    }
    runtime.tool.setButtonVisible(runtime.gv.controlBtn, true)
    runtime.tool.setButtonVisible(
      runtime.gv.picinpicBtn,
      document.pictureInPictureEnabled && runtime.gv.player.nodeName != "OBJECT" && runtime.gv.player.nodeName != "EMBED"
    )
  },
  locate() {
    const playerRect = runtime.tool.getRect(runtime.gv.player)
    if (playerRect.width <= 0 || playerRect.height <= 0) return false
    const client = runtime.tool.getClient()
    const controlWidth = runtime.gv.controlBtn.offsetWidth || 64
    const pipWidth = runtime.gv.picinpicBtn.offsetWidth || 53
    const gap = 4
    const right = runtime.tool.clamp(playerRect.right, controlWidth + pipWidth + gap, client.width)
    const top = runtime.tool.clamp(playerRect.screenY, 0, Math.max(0, client.height - 20))
    runtime.gv.controlBtn.style.opacity = "0.5"
    runtime.gv.controlBtn.innerHTML = runtime.gv.btnText.max
    runtime.gv.controlBtn.style.top = top + "px"
    // 网页全屏按钮位置,Maximize button
    runtime.gv.controlBtn.style.left = right - controlWidth + "px"
    runtime.gv.picinpicBtn.style.opacity = "0.5"
    runtime.gv.picinpicBtn.innerHTML = runtime.gv.btnText.pip
    runtime.gv.picinpicBtn.style.top = runtime.gv.controlBtn.style.top
    // 画中画按钮位置,PicInPic button
    runtime.gv.picinpicBtn.style.left = right - controlWidth - pipWidth - gap + "px"
    return true
  },
  hide() {
    runtime.tool.setButtonVisible(runtime.gv.controlBtn, false)
    runtime.tool.setButtonVisible(runtime.gv.picinpicBtn, false)
    runtime.gv.controlBtn.style.opacity = ""
    runtime.gv.picinpicBtn.style.opacity = ""
  },
  detect() {
    for (const rule in runtime.rules.html5Rules) {
      if (!runtime.tool.matchRule(document.location.hostname, rule)) continue
      for (const selector of runtime.rules.html5Rules[rule]) {
        const player = document.querySelector(selector)
        if (!player) continue
        runtime.gv.player = player
        this.show()
        return
      }
    }
    const videos = [...document.querySelectorAll("video")]
      .filter((video) => video.offsetWidth > 399 && video.offsetHeight > 220)
      .sort((a, b) => b.offsetWidth * b.offsetHeight - a.offsetWidth * a.offsetHeight)
    if (!videos.length) return
    runtime.gv.player = runtime.handle.autoCheck(videos[0]) || videos[0]
    runtime.gv.autoCheckCount = 1
    this.show()
  },
})

/**
 * [INPUT]: 依赖 runtime.gv、runtime.tool、runtime.setButton、runtime.maximize 与播放器规则
 * [OUTPUT]: 对外提供 handle 鼠标、键盘、iframe 消息与画中画处理器
 * [POS]: src 的事件调度层,把浏览器事件转换成全屏/画中画命令
 * [PROTOCOL]: 变更时更新此头部,然后检查 CLAUDE.md
 */

const handle = runtime.register("handle", {
  getPlayer(e) {
    if (runtime.gv.isFull) {
      return
    }
    runtime.gv.mouseoverEl = e.target
    const hostname = document.location.hostname
    let players = []
    for (let i in runtime.rules.html5Rules) {
      if (runtime.tool.matchRule(hostname, i)) {
        for (let html5Rule of runtime.rules.html5Rules[i]) {
          if (document.querySelectorAll(html5Rule).length > 0) {
            for (let player of document.querySelectorAll(html5Rule)) {
              players.push(player)
            }
          }
        }
        break
      }
    }
    if (players.length == 0) {
      for (let generalPlayerRule of runtime.rules.generalPlayerRules) {
        if (document.querySelectorAll(generalPlayerRule).length > 0) {
          for (let player of document.querySelectorAll(generalPlayerRule)) {
            players.push(player)
          }
        }
      }
    }
    if (players.length == 0 && e.target.nodeName != "VIDEO" && document.querySelectorAll("video").length > 0) {
      const videos = document.querySelectorAll("video")
      for (let v of videos) {
        const vRect = v.getBoundingClientRect()
        if (
          e.clientX >= vRect.x - 2 &&
          e.clientX <= vRect.x + vRect.width + 2 &&
          e.clientY >= vRect.y - 2 &&
          e.clientY <= vRect.y + vRect.height + 2 &&
          v.offsetWidth > 399 &&
          v.offsetHeight > 220
        ) {
          players = []
          players[0] = runtime.handle.autoCheck(v)
          runtime.gv.autoCheckCount = 1
          break
        }
      }
    }
    if (players.length > 0) {
      const path = e.path || e.composedPath()
      for (let v of players) {
        if (path.indexOf(v) > -1) {
          runtime.gv.player = v
          runtime.setButton.init()
          return
        }
      }
    }
    switch (e.target.nodeName) {
      case "VIDEO":
      case "OBJECT":
      case "EMBED":
        if (e.target.offsetWidth > 399 && e.target.offsetHeight > 220) {
          runtime.gv.player = e.target
          runtime.setButton.init()
        }
        break
      default:
        runtime.handle.leavePlayer()
    }
  },
  autoCheck(v) {
    let tempPlayer,
      el = v
    runtime.gv.playerChilds = []
    runtime.gv.playerChilds.push(v)
    while ((el = el.parentNode)) {
      if (Math.abs(v.offsetWidth - el.offsetWidth) < 15 && Math.abs(v.offsetHeight - el.offsetHeight) < 15) {
        tempPlayer = el
        runtime.gv.playerChilds.push(el)
      } else {
        break
      }
    }
    return tempPlayer
  },
  leavePlayer() {
    if (runtime.gv.controlBtn.style.visibility == "visible") {
      runtime.gv.controlBtn.style.opacity = "0.35"
      runtime.gv.picinpicBtn.style.opacity = "0.35"
    }
  },
  scrollFix(e) {
    clearTimeout(runtime.gv.scrollFixTimer)
    runtime.gv.scrollFixTimer = setTimeout(() => {
      runtime.setButton.locate()
    }, 20)
  },
  resizeFix() {
    if (!runtime.gv.isFull && runtime.gv.player?.isConnected) runtime.setButton.show()
  },
  hotKey(e) {
    //默认退出键为ESC。需要修改为其他快捷键的请搜索"keycode",修改为按键对应的数字。
    if (e.keyCode == 27) {
      runtime.maximize.playerControl()
    }
    //默认画中画快捷键为F2。
    if (e.keyCode == 113) {
      runtime.handle.pictureInPicture()
    }
  },
  async receiveMessage(e) {
    switch (e.data) {
      case "iframePicInPic":
        runtime.tool.print("messege:iframePicInPic")
        if (!document.pictureInPictureElement) {
          await document
            .querySelector("video")
            .requestPictureInPicture()
            .catch((error) => {
              runtime.tool.addTip(runtime.gv.btnText.tip)
            })
        } else {
          await document.exitPictureInPicture()
        }
        break
      case "iframeVideo":
        runtime.tool.print("messege:iframeVideo")
        if (!runtime.gv.isFull) {
          runtime.gv.player = runtime.gv.mouseoverEl
          runtime.setButton.init()
        }
        break
      case "parentFull":
        runtime.tool.print("messege:parentFull")
        runtime.gv.player = runtime.gv.mouseoverEl
        if (runtime.gv.isIframe) {
          window.parent.postMessage("parentFull", "*")
        }
        runtime.maximize.checkParent()
        runtime.maximize.fullWin()
        if (getComputedStyle(runtime.gv.player).left != "0px") {
          runtime.tool.addStyle(".htmlToothbrush .bodyToothbrush .playerToothbrush {left:0px !important;width:100vw !important;}")
        }
        runtime.gv.isFull = true
        break
      case "parentSmall":
        runtime.tool.print("messege:parentSmall")
        if (runtime.gv.isIframe) {
          window.parent.postMessage("parentSmall", "*")
        }
        runtime.maximize.smallWin()
        break
      case "innerFull":
        runtime.tool.print("messege:innerFull")
        if (runtime.gv.player.nodeName == "IFRAME") {
          runtime.gv.player.contentWindow.postMessage("innerFull", "*")
        }
        runtime.maximize.checkParent()
        runtime.maximize.fullWin()
        break
      case "innerSmall":
        runtime.tool.print("messege:innerSmall")
        if (runtime.gv.player.nodeName == "IFRAME") {
          runtime.gv.player.contentWindow.postMessage("innerSmall", "*")
        }
        runtime.maximize.smallWin()
        break
    }
  },
  pictureInPicture() {
    if (!document.pictureInPictureElement) {
      if (runtime.gv.player) {
        if (runtime.gv.player.nodeName == "IFRAME") {
          runtime.gv.player.contentWindow.postMessage("iframePicInPic", "*")
        } else {
          runtime.gv.player.parentNode.querySelector("video").requestPictureInPicture()
        }
      } else {
        document.querySelector("video").requestPictureInPicture()
      }
    } else {
      document.exitPictureInPicture()
    }
  },
})

/**
 * [INPUT]: 依赖 runtime.gv、runtime.tool、runtime.handle、runtime.setButton 与页面 DOM 树
 * [OUTPUT]: 对外提供 maximize 网页全屏与恢复控制器
 * [POS]: src 的核心状态转换器,负责播放器父链标记、全屏类名与恢复
 * [PROTOCOL]: 变更时更新此头部,然后检查 CLAUDE.md
 */

const maximize = runtime.register("maximize", {
  playerControl() {
    if (!runtime.gv.player) {
      return
    }
    this.checkParent()
    if (!runtime.gv.isFull) {
      if (runtime.gv.isIframe) {
        window.parent.postMessage("parentFull", "*")
      }
      if (runtime.gv.player.nodeName == "IFRAME") {
        runtime.gv.player.contentWindow.postMessage("innerFull", "*")
      }
      this.fullWin()
      if (runtime.gv.autoCheckCount > 0 && !runtime.tool.isHalfFullClient(runtime.gv.playerChilds[0])) {
        if (runtime.gv.autoCheckCount > 10) {
          for (let v of runtime.gv.playerChilds) {
            v.classList.add("videoToothbrush")
          }
          return
        }
        const tempPlayer = runtime.handle.autoCheck(runtime.gv.playerChilds[0])
        runtime.gv.autoCheckCount++
        runtime.maximize.playerControl()
        runtime.gv.player = tempPlayer
        runtime.maximize.playerControl()
      } else {
        runtime.gv.autoCheckCount = 0
      }
    } else {
      if (runtime.gv.isIframe) {
        window.parent.postMessage("parentSmall", "*")
      }
      if (runtime.gv.player.nodeName == "IFRAME") {
        runtime.gv.player.contentWindow.postMessage("innerSmall", "*")
      }
      this.smallWin()
    }
  },
  checkParent() {
    if (runtime.gv.isFull) {
      return
    }
    runtime.gv.playerParents = []
    let full = runtime.gv.player
    while ((full = full.parentNode)) {
      if (full.nodeName == "BODY") {
        break
      }
      if (full.getAttribute) {
        runtime.gv.playerParents.push(full)
      }
    }
  },
  fullWin() {
    if (!runtime.gv.isFull) {
      document.removeEventListener("mouseover", runtime.handle.getPlayer, false)
      runtime.gv.backHtmlId = document.body.parentNode.id
      runtime.gv.backBodyId = document.body.id
      if (document.location.hostname == "www.youtube.com" && !document.querySelector("#player-theater-container #movie_player")) {
        document.querySelector("#movie_player .ytp-size-button").click()
        runtime.gv.ytbStageChange = true
      }
      runtime.gv.leftBtn.style.display = "block"
      runtime.gv.rightBtn.style.display = "block"
      runtime.setButton.hide()
      this.addClass()
    }
    runtime.gv.isFull = true
  },
  addClass() {
    document.documentElement.classList.add("htmlToothbrush")
    document.body.classList.add("bodyToothbrush")
    for (let v of runtime.gv.playerParents) {
      v.classList.add("parentToothbrush")
      //父元素position:fixed会造成层级错乱
      if (getComputedStyle(v).position == "fixed") {
        v.classList.add("absoluteToothbrush")
      }
    }
    runtime.gv.player.classList.add("playerToothbrush")
    if (runtime.gv.player.nodeName == "VIDEO") {
      runtime.gv.backControls = runtime.gv.player.controls
      runtime.gv.player.controls = true
    }
    window.dispatchEvent(new Event("resize"))
  },
  smallWin() {
    document.documentElement.classList.remove("htmlToothbrush")
    document.body.classList.remove("bodyToothbrush")
    for (let v of runtime.gv.playerParents) {
      v.classList.remove("parentToothbrush")
      v.classList.remove("absoluteToothbrush")
    }
    runtime.gv.player.classList.remove("playerToothbrush")
    if (document.location.hostname == "www.youtube.com" && runtime.gv.ytbStageChange && document.querySelector("#player-theater-container #movie_player")) {
      document.querySelector("#movie_player .ytp-size-button").click()
      runtime.gv.ytbStageChange = false
    }
    if (runtime.gv.player.nodeName == "VIDEO") {
      runtime.gv.player.controls = runtime.gv.backControls
    }
    runtime.gv.leftBtn.style.display = ""
    runtime.gv.rightBtn.style.display = ""
    runtime.setButton.hide()
    document.addEventListener("mouseover", runtime.handle.getPlayer, false)
    window.dispatchEvent(new Event("resize"))
    runtime.gv.isFull = false
  },
})

/**
 * [INPUT]: 依赖 runtime.gv、runtime.tool、runtime.handle、runtime.setButton、runtime.antiBlurPause 与页面 body
 * [OUTPUT]: 对外提供 init 启动函数与 DOMContentLoaded 引导逻辑
 * [POS]: src 的应用入口,装配按钮、样式、事件监听与延迟探测
 * [PROTOCOL]: 变更时更新此头部,然后检查 CLAUDE.md
 */

const init = runtime.register("init", () => {
  runtime.antiBlurPause.init()
  document.documentElement.classList.remove("htmlToothbrush")
  document.body.classList.remove("bodyToothbrush")
  document.querySelectorAll(".parentToothbrush, .absoluteToothbrush, .playerToothbrush, .videoToothbrush").forEach((element) => {
    element.classList.remove("parentToothbrush", "absoluteToothbrush", "playerToothbrush", "videoToothbrush")
  })
  ;["picinpicBtn", "playerControlBtn", "leftFullStackButton", "rightFullStackButton"].forEach((id) => {
    document.getElementById(id)?.remove()
  })
  runtime.gv.picinpicBtn = document.createElement("div")
  runtime.gv.picinpicBtn.id = "picinpicBtn"
  runtime.gv.picinpicBtn.textContent = runtime.gv.btnText.pip
  runtime.tool.styleFloatingButton(runtime.gv.picinpicBtn, 53)
  runtime.tool.setButtonVisible(runtime.gv.picinpicBtn, false)
  runtime.gv.picinpicBtn.onclick = () => {
    runtime.handle.pictureInPicture()
  }
  document.body.appendChild(runtime.gv.picinpicBtn)
  runtime.gv.controlBtn = runtime.tool.createButton("playerControlBtn", true)
  runtime.gv.controlBtn.textContent = runtime.gv.btnText.max
  runtime.gv.leftBtn = runtime.tool.createButton("leftFullStackButton")
  runtime.gv.rightBtn = runtime.tool.createButton("rightFullStackButton")

  if (!document.getElementById("maximizeVideoStyle")) {
    const style = runtime.tool.addStyle(
      [
        ".htmlToothbrush .bodyToothbrush .parentToothbrush .bilibili-player-video {margin:0 !important;}",
        ".htmlToothbrush, .bodyToothbrush {overflow:hidden !important;zoom:100% !important;}",
        ".htmlToothbrush .bodyToothbrush .parentToothbrush {overflow:visible !important;z-index:2147483646 !important;transform:none !important;filter:none !important;isolation:auto !important;-webkit-transform-style:flat !important;transition:none !important;contain:none !important;}",
        ".htmlToothbrush .bodyToothbrush .absoluteToothbrush {position:absolute !important;}",
        ".htmlToothbrush .bodyToothbrush > *:not(.parentToothbrush):not(.playerToothbrush):not(#playerControlBtn):not(#picinpicBtn):not(#leftFullStackButton):not(#rightFullStackButton) {visibility:hidden !important;}",
        ".htmlToothbrush .bodyToothbrush .parentToothbrush > *:not(.parentToothbrush):not(.playerToothbrush) {visibility:hidden !important;}",
        ".htmlToothbrush .bodyToothbrush .playerToothbrush {visibility:visible !important;position:fixed !important;top:0px !important;left:0px !important;width:100vw !important;height:100vh !important;max-width:none !important;max-height:none !important;min-width:0 !important;min-height:0 !important;margin:0 !important;padding:0 !important;z-index:2147483647 !important;border:none !important;background-color:#000 !important;transform:none !important;filter:none !important;contain:none !important;}",
        ".htmlToothbrush .bodyToothbrush .parentToothbrush video {object-fit:contain !important;}",
        ".htmlToothbrush .bodyToothbrush .parentToothbrush .videoToothbrush {width:100vw !important;height:100vh !important;}",
        '#playerControlBtn {text-shadow: none;visibility:hidden;opacity:0;display:none;transition: all 0.5s ease;cursor: pointer;font: 12px "微软雅黑";margin:0;width:64px;height:20px;line-height:20px;border:none;text-align: center;position: fixed;z-index:2147483647;background-color: #27A9D8;color: #FFF;} #playerControlBtn:hover {visibility:visible;opacity:1;background-color:#2774D8;}',
        '#picinpicBtn {text-shadow: none;visibility:hidden;opacity:0;display:none;transition: all 0.5s ease;cursor: pointer;font: 12px "微软雅黑";margin:0;width:53px;height:20px;line-height:20px;border:none;text-align: center;position: fixed;z-index:2147483647;background-color: #27A9D8;color: #FFF;} #picinpicBtn:hover {visibility:visible;opacity:1;background-color:#2774D8;}',
        "#leftFullStackButton{display:none;position:fixed;width:1px;height:100vh;top:0;left:0;z-index:2147483647;background:#000;}",
        "#rightFullStackButton{display:none;position:fixed;width:1px;height:100vh;top:0;right:0;z-index:2147483647;background:#000;}",
      ].join("\n")
    )
    style.id = "maximizeVideoStyle"
  }
  document.addEventListener("mouseover", runtime.handle.getPlayer, false)
  document.addEventListener("keydown", runtime.handle.hotKey, false)
  window.addEventListener("message", runtime.handle.receiveMessage, false)
  window.addEventListener("resize", runtime.handle.resizeFix, false)
  ;[400, 1200, 3000].forEach((delay) => setTimeout(() => runtime.setButton.detect(), delay))
  runtime.tool.print("Ready")
})

runtime.antiBlurPause.init()
if (document.body) {
  init()
} else {
  document.addEventListener("DOMContentLoaded", init, { once: true })
}
})()