Telegram 網頁全螢幕

為網頁Telegram添加視頻播放網頁全螢幕功能

// ==UserScript==
// @name:zh-CN  Telegram 网页全屏
// @name:zh-TW  Telegram 網頁全螢幕
// @name:ja     Telegram ウェブ全画面
// @name        Telegram in-page fullscree
// @namespace    http://tampermonkey.net/
// @version      1.2
// @icon         https://img.icons8.com/color/452/telegram-app--v5.png
// @description:zh-CN  为网页Telegram添加视频播放网页全屏功能
// @description:zh-TW  為網頁Telegram添加視頻播放網頁全螢幕功能
// @description:ja     ウェブ版Telegramに動画再生の全画面機能を追加
// @description  Add browser viewport full video playback feature for WebTelegram
// @author       zolay-poi
// @match        https://web.telegram.org/*
// @grant        none
// @run-at       document-idle
// @license MIT
// ==/UserScript==

(function () {
  "use strict";

  const i18n = {
    en: { enter: "Web Fullscreen", exit: "Exit Web Fullscreen" },
    "zh-CN": { enter: "网页全屏", exit: "退出网页全屏" },
    "zh-TW": { enter: "網頁全螢幕", exit: "退出網頁全螢幕" },
    ja: { enter: "ウェブ全画面", exit: "ウェブ全画面を終了" },
  };

  const language = navigator.language || navigator.userLanguage || "en";
  const baseLang = language.split("-")[0];
  const resolvedLang =
    (i18n[language] && language) ||
    (i18n[baseLang] && baseLang) ||
    (baseLang === "zh" ? "zh-CN" : "en");

  const h = (key) => i18n[resolvedLang][key];

  const doms = {
    btn: null,
    span: null,
  };

  const btnCls = "my-btn-dom-fullscreen";
  const fullCls = "tg-inner-fs";
  const styleId = "tg-inner-fs-style-tag";

  const CONTAINER_SELECTORS = [
    "body div.media-viewer-whole.active div.right-controls",
    "div.media-viewer-whole.active .right-controls",
    ".media-viewer-whole .right-controls",
    ".right-controls",
  ];

  const TARGET_SELECTORS = [
    "div.media-viewer-whole.active .media-viewer-mover.center.active",
    ".media-viewer-whole.active .media-viewer-mover.center.active",
    ".media-viewer-whole.active .media-viewer-mover.center",
    ".media-viewer-whole .media-viewer-mover.center.active",
    ".media-viewer-mover.center.active",
  ];

  function findTarget() {
    for (const sel of TARGET_SELECTORS) {
      const el = document.querySelector(sel);
      if (el) return el;
    }
    return null;
  }

  function findContainer() {
    for (const sel of CONTAINER_SELECTORS) {
      const el = document.querySelector(sel);
      if (el) return el;
    }
    return null;
  }

  const spanIcons = {
    enter: "\ue948",
    exit: "\ue968",
  };

  function ensureStyle() {
    if (document.getElementById(styleId)) return;
    const style = document.createElement("style");
    style.id = styleId;
    style.textContent = `
      .${fullCls} .media-viewer-topbar,
      .${fullCls} .media-viewer-caption {
        display: none !important;
      }
      .${fullCls} .media-viewer-mover.center {
        width: 100% !important;
        height: 100% !important;
        max-height: 100% !important;
      }
    `;
    document.head.appendChild(style);
  }

  function ensureDoms() {
    if (!doms.btn || !(doms.btn instanceof HTMLElement)) {
      const btn = document.createElement("div");
      btn.title = h("enter");
      btn.className = btnCls + " btn-icon default__button";

      const span = document.createElement("span");
      span.className = "tgico button-icon";
      span.innerText = spanIcons.enter;
      btn.appendChild(span);

      btn.addEventListener("click", function () {
        const target = findTarget();
        if (!target) return;

        const isFull = btn.dataset.fullscreen === "1";
        if (isFull) {
          document.body.classList.remove(fullCls);
          btn.dataset.fullscreen = "0";
          doms.span && (doms.span.innerText = spanIcons.enter);
          btn.title = h("enter");
        } else {
          document.body.classList.add(fullCls);
          btn.dataset.fullscreen = "1";
          doms.span && (doms.span.innerText = spanIcons.exit);
          btn.title = h("exit");
        }
      });

      doms.btn = btn;
      doms.span = span;
    } else if (!doms.span || !(doms.span instanceof HTMLElement)) {
      const existed = doms.btn.querySelector("span.tgico.button-icon");
      if (existed) doms.span = existed;
    }
    return doms;
  }

  function insertBeforeLastChild(container, node) {
    const last = container.lastElementChild;
    if (last) {
      container.insertBefore(node, last);
    } else {
      container.appendChild(node);
    }
  }

  function tryInsert() {
    const container = findContainer();
    if (!container) return false;
    ensureStyle();
    const existed = container.querySelector("." + btnCls);
    const { btn } = ensureDoms();
    if (existed || container.contains(btn)) return true;
    insertBeforeLastChild(container, btn);
    return true;
  }

  let pending = false;
  function scheduleTryInsert() {
    if (pending) return;
    pending = true;
    (window.requestAnimationFrame || window.setTimeout)(() => {
      pending = false;
      tryInsert();
    }, 16);
  }

  tryInsert();

  const observer = new MutationObserver((mutations) => {
    for (const m of mutations) {
      if (m.type === "childList" && (m.addedNodes.length || m.removedNodes.length)) {
        scheduleTryInsert();
        break;
      }
      if (m.type === "attributes" && m.attributeName === "class") {
        const t = m.target;
        if (t && t.nodeType === 1) {
          const el = t;
          if (el.matches && (el.matches(".media-viewer-whole") || el.matches(".right-controls"))) {
            scheduleTryInsert();
            break;
          }
        }
      }
    }
  });
  observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ["class"] });

  window.addEventListener("beforeunload", () => observer.disconnect());
})();