YouTube Theater Plus

为 YouTube 剧场模式增强功能,如全屏剧场、自动开启剧场等,并支持新界面

  1. // ==UserScript==
  2. // @name YouTube Theater Plus
  3. // @name:en YouTube Theater Plus
  4. // @name:id YouTube Theater Plus
  5. // @name:zh-CN YouTube Theater Plus
  6. // @name:zh-TW YouTube Theater Plus
  7. // @name:ja YouTube Theater Plus
  8. // @name:ko YouTube Theater Plus
  9. // @name:fr YouTube Theater Plus
  10. // @name:es YouTube Theater Plus
  11. // @name:de YouTube Theater Plus
  12. // @name:ru YouTube Theater Plus
  13. // @description Enhances YouTube Theater with features like Fullpage Theater, Auto Open Theater, and more, including support for the new UI
  14. // @description:en Enhances YouTube Theater with features like Fullpage Theater, Auto Open Theater, and more, including support for the new UI
  15. // @description:id Tingkatkan YouTube Theater dengan fitur seperti Fullpage Theater, Auto Open Theater, dan lainnya, termasuk dukungan untuk tampilan baru
  16. // @description:zh-CN 为 YouTube 剧场模式增强功能,如全屏剧场、自动开启剧场等,并支持新界面
  17. // @description:zh-TW 為 YouTube 劇場模式增強功能,如全螢幕劇場、自動開啟劇場等,並支援新介面
  18. // @description:ja YouTube シアターモードを拡張し、フルページシアター、自動シアター起動などの機能を追加し、新しい UI に対応します
  19. // @description:ko YouTube 시어터 모드를 강화하여 전체 페이지 시어터, 자동 시어터 열기 등 다양한 기능을 제공하며 새로운 UI도 지원합니다
  20. // @description:fr Améliore YouTube Theater avec des fonctionnalités comme le mode plein écran, l’ouverture automatique et d’autres, y compris la prise en charge de la nouvelle interface
  21. // @description:es Mejora YouTube Theater con funciones como el modo de pantalla completa, apertura automática y más, incluyendo soporte para la nueva interfaz
  22. // @description:de Erweitert YouTube Theater mit Funktionen wie Vollseiten-Theater, automatischem Öffnen und weiteren, einschließlich Unterstützung für die neue Benutzeroberfläche
  23. // @description:ru Расширяет YouTube Theater функциями, такими как полноэкранный режим, автоматическое открытие и другими, включая поддержку нового интерфейса
  24. // @version 2.4.5
  25. // @run-at document-body
  26. // @inject-into content
  27. // @match https://www.youtube.com/*
  28. // @exclude https://*.youtube.com/live_chat*
  29. // @exclude https://*.youtube.com/embed*
  30. // @exclude https://*.youtube.com/tv*
  31. // @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com
  32. // @grant GM.getValue
  33. // @grant GM.setValue
  34. // @author Fznhq
  35. // @namespace https://github.com/fznhq
  36. // @homepageURL https://github.com/fznhq/userscript-collection
  37. // @homepage https://github.com/fznhq/userscript-collection
  38. // @license GNU GPLv3
  39. // ==/UserScript==
  40.  
  41. // Icons provided by https://iconmonstr.com/
  42.  
  43. (async function () {
  44. "use strict";
  45.  
  46. const html = document.documentElement;
  47. const body = document.body;
  48.  
  49. let theater = false;
  50. let fullpage = false;
  51. let headerOpen = false;
  52.  
  53. /**
  54. * @typedef {object} Option
  55. * @property {string} icon
  56. * @property {string} label
  57. * @property {any} value
  58. * @property {Function} onUpdate
  59. * @property {Option} sub
  60. */
  61.  
  62. /**
  63. * Options must be changed via popup menu,
  64. * just press (v) to open the menu
  65. */
  66. const subIcon = `{"svg":{"clip-rule":"evenodd","fill-rule":"evenodd","stroke-linejoin":"round","stroke-miterlimit":"2"},"path":{"d":"M10.211 7.155A.75.75 0 0 0 9 7.747v8.501a.75.75 0 0 0 1.212.591l5.498-4.258a.746.746 0 0 0-.001-1.183zm.289 7.563V9.272l3.522 2.719z","fill-rule":"nonzero"}}`;
  67. const options = {
  68. fullpage_theater: {
  69. icon: `{"path":{"d":"M22 4v12H2V4zm1-1H1v14h22zm-6 17H7v1h10z"}}`,
  70. label: "Fullpage Theater;", // Remove ";" to set your own label.
  71. value: true,
  72. onUpdate() {
  73. if (theater) applyTheaterMode(true);
  74. },
  75. sub: {
  76. show_title: {
  77. label: "In Player Title;", // Remove ";" to set your own label.
  78. value: false,
  79. onUpdate() {
  80. if (fullpage) setHtmlAttr(attr.show_title, this.value);
  81. },
  82. },
  83. },
  84. },
  85. auto_theater_mode: {
  86. icon: `{"svg":{"fill-rule":"evenodd","clip-rule":"evenodd"},"path":{"d":"M24 22H0V2h24zm-7-1V6H1v15zm1 0h5V3H1v2h17zm-6-6h-1v-3l-7 7-1-1 7-7H7v-1h5z"}}`,
  87. label: "Auto Open Theater;", // Remove ";" to set your own label.
  88. value: false,
  89. onUpdate() {
  90. if (this.value && !theater) toggleTheater();
  91. },
  92. },
  93. hide_scrollbar: {
  94. icon: `{"path":{"d":"M14 12a2 2 0 1 1-4 0 2 2 0 0 1 4 0m-3-4h2V6h4l-5-6-5 6h4zm2 8h-2v2H7l5 6 5-6h-4z"}}`,
  95. label: "Theater Hide Scrollbar;", // Remove ";" to set your own label.
  96. value: true,
  97. onUpdate() {
  98. if (theater) {
  99. setHtmlAttr(attr.no_scroll, this.value);
  100. resizeWindow();
  101. }
  102. },
  103. },
  104. close_theater_with_esc: {
  105. icon: `{"svg":{"clip-rule":"evenodd","fill-rule":"evenodd","stroke-linejoin":"round","stroke-miterlimit":2},"path":{"d":"M21 3.998c0-.478-.379-1-1-1H5c-.62 0-1 .519-1 1v15c0 .621.52 1 1 1h15c.478 0 1-.379 1-1zm-16 0h15v15H5zm7.491 6.432 2.717-2.718a.75.75 0 0 1 1.061 1.062l-2.717 2.717 2.728 2.728a.75.75 0 1 1-1.061 1.062l-2.728-2.728-2.728 2.728a.751.751 0 0 1-1.061-1.062l2.728-2.728-2.722-2.722a.75.75 0 0 1 1.061-1.061z","fill-rule":"nonzero"}}`,
  106. label: "Close Theater With Esc;", // Remove ";" to set your own label.
  107. value: true,
  108. },
  109. hide_cards: {
  110. icon: `{"path":{"d":"M22 6v16H6V6zm1-1H5v18h18zM2 2v20h1V3h18V2zm12 9c-3 0-5 3-5 3s2 3 5 3 5-3 5-3-2-3-5-3m0 5a2 2 0 1 1 0-4 2 2 0 0 1 0 4m1-2a1 1 0 1 1-2 0 1 1 0 0 0 1-1l1 1"}}`,
  111. label: "Hide Cards;", // Remove ";" to set your own label.
  112. value: true,
  113. onUpdate() {
  114. setHtmlAttr(attr.hide_card, this.value);
  115. },
  116. },
  117. show_header_near: {
  118. icon: `{"path":{"d":"M5 4.27 15.476 13H8.934L5 18.117zm-1 0v17l5.5-7h9L4 1.77z"}}`,
  119. label: "Show Header When Mouse is Near;", // Remove ";" to set your own label.
  120. value: false,
  121. sub: {
  122. trigger_area: {
  123. label: "Trigger Area;", // Remove ";" to set your own label.
  124. value: 200,
  125. },
  126. delay: {
  127. label: "Delay (in milliseconds);", // Remove ";" to set your own label.
  128. value: 0,
  129. },
  130. },
  131. },
  132. };
  133.  
  134. function resizeWindow() {
  135. document.dispatchEvent(new Event("resize", { bubbles: true }));
  136. }
  137.  
  138. /**
  139. * @param {string} name
  140. * @param {object} attributes
  141. * @param {Array} append
  142. * @returns {SVGElement}
  143. */
  144. function createNS(name, attributes = {}, append = []) {
  145. const el = document.createElementNS("http://www.w3.org/2000/svg", name);
  146. for (const k in attributes) el.setAttributeNS(null, k, attributes[k]);
  147. return el.append(...append), el;
  148. }
  149.  
  150. /**
  151. * @param {string} name
  152. * @param {any} value
  153. * @param {Option} option
  154. * @returns {any}
  155. */
  156. function saveOption(name, value, option) {
  157. GM.setValue(name, value);
  158. return (option.value = value);
  159. }
  160.  
  161. /**
  162. * @param {string} name
  163. * @param {string} subName
  164. */
  165. function optionKey(name, subName) {
  166. return subName ? `${name}_sub_${subName}` : name;
  167. }
  168.  
  169. /**
  170. * @param {string} name
  171. * @param {string} subName
  172. */
  173. async function loadOption(name, subName) {
  174. const key = optionKey(name, subName);
  175. const keyLabel = `label_${key}`;
  176. /** @type {Option} */
  177. const option = subName ? options[name].sub[subName] : options[name];
  178. option.value = await GM.getValue(key, option.value);
  179.  
  180. let label = option.label;
  181. if (label.endsWith(";")) label = await GM.getValue(keyLabel, label);
  182. else GM.setValue(keyLabel, label);
  183. option.label = label.replace(/;$/, "");
  184.  
  185. const icon = JSON.parse(option.icon || subIcon);
  186. option.icon = createNS("svg", icon.svg, [createNS("path", icon.path)]);
  187. }
  188.  
  189. for (const name in options) {
  190. await loadOption(name);
  191. for (const subName in options[name].sub) {
  192. await loadOption(name, subName);
  193. }
  194. }
  195.  
  196. /**
  197. * @param {string} className
  198. * @param {Array} append
  199. * @returns {HTMLDivElement}
  200. */
  201. function createDiv(className, append = []) {
  202. const el = document.createElement("div");
  203. el.className = "ytp-menuitem" + (className ? "-" + className : "");
  204. return el.append(...append), el;
  205. }
  206.  
  207. /**
  208. * @param {string} name
  209. * @param {Option} option
  210. * @returns {HTMLInputElement}
  211. */
  212. function itemInput(name, option) {
  213. const input = document.createElement("input");
  214. const val = () => Number(input.value.replace(/\D/g, ""));
  215. const setValue = (value) => (input.value = value);
  216.  
  217. setValue(option.value);
  218.  
  219. input.addEventListener("input", () => setValue(val()));
  220. input.addEventListener("change", () => saveOption(name, val(), option));
  221.  
  222. return input;
  223. }
  224.  
  225. /** @type {Map<HTMLElement, HTMLElement[]>} */
  226. const menuItems = new Map();
  227.  
  228. /**
  229. * @param {HTMLElement} item
  230. * @param {boolean} checked
  231. */
  232. function toggleItemSub(item, checked) {
  233. for (const itemSub of menuItems.get(item)) {
  234. itemSub.style.display = checked ? "" : "none";
  235. }
  236. }
  237.  
  238. /**
  239. * @param {string} name
  240. * @param {Option} option
  241. * @returns {HTMLElement}
  242. */
  243. function createItem(name, option) {
  244. const checkbox = typeof option.value === "boolean";
  245. const isSub = name.includes("sub_");
  246. const icon = isSub ? [] : [option.icon];
  247. const label = isSub
  248. ? [createDiv("icon", [option.icon]), option.label]
  249. : [option.label];
  250. const content = checkbox
  251. ? [createDiv("toggle-checkbox")]
  252. : [itemInput(name, option)];
  253. const item = createDiv("", [
  254. createDiv("icon", icon),
  255. createDiv("label", label),
  256. createDiv("content", content),
  257. ]);
  258.  
  259. if (checkbox) {
  260. item.setAttribute("aria-checked", option.value);
  261. item.addEventListener("click", () => {
  262. const checked = saveOption(name, !option.value, option);
  263. item.setAttribute("aria-checked", checked);
  264. if (!isSub) toggleItemSub(item, checked);
  265. if (option.onUpdate) option.onUpdate();
  266. });
  267. }
  268.  
  269. return item;
  270. }
  271.  
  272. const popup = {
  273. show: false,
  274. menu: (() => {
  275. const menu = createDiv(" ytc-menu ytp-panel-menu");
  276. const container = createDiv(" ytc-popup-container", [menu]);
  277.  
  278. for (const name in options) {
  279. const option = options[name];
  280. const item = createItem(name, option);
  281. menuItems.set(menu.appendChild(item), []);
  282.  
  283. for (const subName in option.sub) {
  284. const subOption = option.sub[subName];
  285. const sub = createItem(optionKey(name, subName), subOption);
  286. menuItems.get(item).push(menu.appendChild(sub));
  287. }
  288.  
  289. toggleItemSub(item, option.value);
  290. }
  291.  
  292. window.addEventListener("click", (ev) => {
  293. if (popup.show && !menu.contains(ev.target)) {
  294. popup.show = !!container.remove();
  295. }
  296. });
  297.  
  298. return container;
  299. })(),
  300. };
  301.  
  302. window.addEventListener("keydown", (ev) => {
  303. const isPressV = ev.key.toLowerCase() === "v" || ev.code === "KeyV";
  304.  
  305. if (
  306. (isPressV && !ev.ctrlKey && !isActiveEditable()) ||
  307. (ev.code === "Escape" && popup.show)
  308. ) {
  309. document.activeElement.blur();
  310. popup.show = popup.show
  311. ? !!popup.menu.remove()
  312. : !body.append(popup.menu);
  313. }
  314. });
  315.  
  316. /**
  317. * @param {string} query
  318. * @returns {() => HTMLElement | null}
  319. */
  320. function $(query) {
  321. let element = null;
  322. return () => element || (element = document.querySelector(query));
  323. }
  324.  
  325. const style = document.head.appendChild(document.createElement("style"));
  326. style.textContent = /*css*/ `
  327. html[no-scroll],
  328. html[no-scroll] body {
  329. scrollbar-width: none !important;
  330. }
  331.  
  332. html[no-scroll]::-webkit-scrollbar,
  333. html[no-scroll] body::-webkit-scrollbar,
  334. html[hide-card] ytd-player .ytp-paid-content-overlay,
  335. html[hide-card] ytd-player .iv-branding,
  336. html[hide-card] ytd-player .ytp-ce-element,
  337. html[hide-card] ytd-player .ytp-suggested-action {
  338. display: none !important;
  339. }
  340.  
  341. html[chat-hidden] #panels-full-bleed-container {
  342. display: none;
  343. }
  344.  
  345. html[masthead-hidden] #masthead-container {
  346. transform: translateY(-100%) !important;
  347. }
  348.  
  349. html[masthead-hidden] [fixed-panels] #chat {
  350. top: 0 !important;
  351. }
  352.  
  353. html[theater] #page-manager {
  354. margin: 0 !important;
  355. }
  356.  
  357. html[theater] #content #page-manager ytd-watch-flexy #full-bleed-container,
  358. html[theater] #content #page-manager ytd-watch-grid #player-full-bleed-container {
  359. height: 100vh;
  360. min-height: auto;
  361. max-height: none;
  362. }
  363.  
  364. html[show-title] .ytp-chrome-top {
  365. height: auto !important;
  366. }
  367.  
  368. html[show-title] .ytp-title,
  369. html[show-title] .ytp-gradient-top {
  370. display: flex !important;
  371. }
  372.  
  373. .ytc-popup-container {
  374. position: fixed;
  375. inset: 0;
  376. z-index: 9000;
  377. background: rgba(0, 0, 0, 0.5);
  378. display: flex;
  379. align-items: center;
  380. justify-content: center;
  381. }
  382.  
  383. .ytc-menu.ytp-panel-menu {
  384. background: var(--yt-spec-base-background, #0f0f0f);
  385. width: 400px;
  386. font-size: 120%;
  387. padding: 10px;
  388. fill: #eee;
  389. }
  390.  
  391. .ytc-menu input {
  392. width: 36px;
  393. text-align: center;
  394. }
  395.  
  396. .ytc-menu .ytp-menuitem-label .ytp-menuitem-icon {
  397. display: inline-block;
  398. padding: 0 10px 0 0;
  399. margin-left: -10px;
  400. }
  401.  
  402. .ytc-menu .ytp-menuitem-toggle-checkbox::after {
  403. background: #fff !important;
  404. }
  405.  
  406. .ytc-menu .ytp-menuitem-icon {
  407. fill: var(--yt-spec-brand-icon-active, #fff) !important;
  408. }
  409.  
  410. .ytc-menu .ytp-menuitem-label {
  411. color: var(--yt-spec-text-primary, #f1f1f1) !important;
  412. }
  413.  
  414. .ytc-menu [aria-checked=false] .ytp-menuitem-toggle-checkbox {
  415. background: #5b5b5b !important;
  416. }
  417. `;
  418.  
  419. const attrName = "yttp-" + Date.now().toString(36);
  420. const attr = {
  421. video_id: "video-id",
  422. role: "role",
  423. theater: "theater",
  424. fullscreen: "fullscreen",
  425. hidden_header: "masthead-hidden",
  426. no_scroll: "no-scroll",
  427. hide_card: "hide-card",
  428. chat_hidden: "chat-hidden",
  429. show_title: "show-title",
  430. };
  431.  
  432. for (const key in attr) {
  433. style.textContent = style.textContent.replaceAll(
  434. `[${attr[key]}]`,
  435. `[${attrName}~="${attr[key]}"]`
  436. );
  437. }
  438.  
  439. const element = {
  440. watch: $("ytd-watch-flexy, ytd-watch-grid"), // ytd-watch-grid === trash
  441. search: $("form[action*=result] input"),
  442. };
  443.  
  444. const keyToggleTheater = new KeyboardEvent("keydown", {
  445. key: "t",
  446. code: "KeyT",
  447. which: 84,
  448. keyCode: 84,
  449. bubbles: true,
  450. cancelable: true,
  451. });
  452.  
  453. const tempAttrs = document.createElement("a");
  454.  
  455. /**
  456. * @param {string} attr
  457. * @param {boolean} state
  458. */
  459. function setHtmlAttr(attr, state) {
  460. tempAttrs.classList.toggle(attr, state);
  461. html.setAttribute(attrName, tempAttrs.className);
  462. }
  463.  
  464. /**
  465. * @param {MutationCallback} callback
  466. * @param {Node} target
  467. * @param {MutationObserverInit | undefined} options
  468. */
  469. function observer(callback, target, options) {
  470. const mutation = new MutationObserver(callback);
  471. mutation.observe(target, options || { subtree: true, childList: true });
  472. }
  473.  
  474. /**
  475. * @returns {boolean}
  476. */
  477. function isTheater() {
  478. const watch = element.watch();
  479. return (
  480. watch.getAttribute(attr.role) === "main" &&
  481. watch.hasAttribute(attr.theater) &&
  482. !watch.hasAttribute(attr.fullscreen)
  483. );
  484. }
  485.  
  486. /**
  487. * @returns {boolean}
  488. */
  489. function isActiveEditable() {
  490. /** @type {HTMLElement} */
  491. const active = document.activeElement;
  492. return (
  493. active.tagName === "TEXTAREA" ||
  494. active.tagName === "INPUT" ||
  495. active.isContentEditable
  496. );
  497. }
  498.  
  499. /**
  500. * @param {boolean} state
  501. * @param {number} timeout
  502. * @param {Function} callback
  503. * @returns {number | boolean}
  504. */
  505. function toggleHeader(state, timeout, callback) {
  506. const toggle = () => {
  507. if (!fullpage) return;
  508. if (state || document.activeElement !== element.search()) {
  509. const showNear = options.show_header_near.value;
  510. headerOpen = state || (!showNear && !!window.scrollY);
  511. setHtmlAttr(attr.hidden_header, !headerOpen);
  512. if (callback) callback();
  513. }
  514. };
  515. return fullpage && setTimeout(toggle, timeout || 1);
  516. }
  517.  
  518. let mouseNearDelayId = 0;
  519. let mouseNearTimerId = 0;
  520.  
  521. /**
  522. * @param {number} delay
  523. * @returns {number}
  524. */
  525. function mouseNearHide(delay = 0) {
  526. return toggleHeader(false, delay, () => {
  527. clearTimeout(mouseNearDelayId);
  528. mouseNearDelayId = 0;
  529. });
  530. }
  531.  
  532. function mouseNearToggle(/** @type {MouseEvent} */ ev) {
  533. if (options.show_header_near.value && fullpage) {
  534. const subOptions = options.show_header_near.sub;
  535. const area = subOptions.trigger_area.value;
  536. const state = !popup.show && ev.clientY < area;
  537. const delay = headerOpen ? 0 : subOptions.delay.value;
  538.  
  539. if (state && (!mouseNearDelayId || headerOpen)) {
  540. clearTimeout(mouseNearTimerId);
  541. mouseNearTimerId = mouseNearHide(delay + 1500);
  542. mouseNearDelayId = toggleHeader(true, delay);
  543. } else if (!state) mouseNearHide();
  544. }
  545. }
  546.  
  547. function toggleTheater() {
  548. document.dispatchEvent(keyToggleTheater);
  549. }
  550.  
  551. function onEscapePress(/** @type {KeyboardEvent} */ ev) {
  552. if (ev.code !== "Escape" || !theater || popup.show) return;
  553.  
  554. const input = element.search();
  555.  
  556. if (options.close_theater_with_esc.value) toggleTheater();
  557. else if (document.activeElement !== input) input.focus();
  558. else input.blur();
  559. }
  560.  
  561. function registerEventListener() {
  562. window.addEventListener("mousemove", mouseNearToggle);
  563. window.addEventListener("keydown", onEscapePress, true);
  564. window.addEventListener("mouseout", (ev) => {
  565. if (ev.clientY <= 0) mouseNearHide();
  566. });
  567. window.addEventListener("scroll", () => {
  568. if (!options.show_header_near.value) toggleHeader();
  569. });
  570. element.search().addEventListener("focus", () => toggleHeader(true));
  571. element.search().addEventListener("blur", () => toggleHeader(false));
  572. }
  573.  
  574. /**
  575. * @param {boolean | undefined} force
  576. */
  577. function applyTheaterMode(force) {
  578. const state = isTheater();
  579.  
  580. if (theater === state && !force) return;
  581.  
  582. const opt_ft = options.fullpage_theater;
  583. theater = state;
  584. fullpage = theater && opt_ft.value;
  585.  
  586. setHtmlAttr(attr.theater, fullpage);
  587. setHtmlAttr(attr.hidden_header, fullpage);
  588. setHtmlAttr(attr.show_title, fullpage && opt_ft.sub.show_title.value);
  589. setHtmlAttr(attr.no_scroll, theater && options.hide_scrollbar.value);
  590. setHtmlAttr(attr.hide_card, options.hide_cards.value);
  591. resizeWindow();
  592. }
  593.  
  594. /**
  595. * @param {MutationRecord[]} mutations
  596. */
  597. function autoOpenTheater(mutations) {
  598. const attrs = [attr.role, attr.video_id, attrName];
  599. const watch = element.watch();
  600.  
  601. if (
  602. !theater &&
  603. options.auto_theater_mode.value &&
  604. watch.getAttribute(attr.video_id) &&
  605. !watch.hasAttribute(attr.fullscreen) &&
  606. mutations.some((m) => attrs.includes(m.attributeName))
  607. ) {
  608. setTimeout(toggleTheater, 1);
  609. }
  610. }
  611.  
  612. /**
  613. * @returns {boolean}
  614. */
  615. function isChatHidden() {
  616. const chat = document.getElementById("chat");
  617. const frame = chat?.querySelector("iframe");
  618.  
  619. if (
  620. frame &&
  621. chat.offsetHeight &&
  622. frame.offsetHeight &&
  623. element.watch().hasAttribute("fixed-panels")
  624. ) {
  625. const styleChat = getComputedStyle(chat);
  626.  
  627. if (
  628. styleChat.position === "fixed" &&
  629. styleChat.visibility !== "hidden" &&
  630. Number(styleChat.opacity)
  631. ) {
  632. return false;
  633. }
  634. }
  635.  
  636. return !!chat;
  637. }
  638.  
  639. let chatState = false;
  640.  
  641. function observeChatChange() {
  642. const state = isChatHidden();
  643.  
  644. if (state !== chatState) {
  645. setHtmlAttr(attr.chat_hidden, (chatState = state));
  646. resizeWindow();
  647. }
  648. }
  649.  
  650. observer(observeChatChange, document, {
  651. subtree: true,
  652. childList: true,
  653. attributes: true,
  654. });
  655.  
  656. observer((_, observe) => {
  657. const watch = element.watch();
  658.  
  659. if (watch) {
  660. observe.disconnect();
  661. observer(
  662. (mutations) => {
  663. applyTheaterMode();
  664. autoOpenTheater(mutations);
  665. },
  666. watch,
  667. { attributes: true }
  668. );
  669. watch.setAttribute(attrName, "");
  670. registerEventListener();
  671. }
  672. }, body);
  673. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址