Maximize Video Fixed

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

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==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 })
}
})()