Telegram in-page fullscree

Add browser viewport full video playback feature for WebTelegram

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name:zh-CN  Telegram 网页全屏
// @name:zh-TW  Telegram 網頁全螢幕
// @name:ja     Telegram ウェブ全画面
// @name        Telegram in-page fullscree
// @namespace    http://tampermonkey.net/
// @version      1.3
// @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: "\ue969",
    exit: "\ue948",
  };

  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());
})();