Poki Fullscreen Button (Always On)

Adds a fullscreen button to poki.com when missing, keeps it alive across SPA navigation and fullscreen toggles without heavy DOM observers.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Poki Fullscreen Button (Always On)
// @namespace    gg.poki.fullscreen
// @version      1.3.0
// @description  Adds a fullscreen button to poki.com when missing, keeps it alive across SPA navigation and fullscreen toggles without heavy DOM observers.
// @match        https://poki.com/*
// @run-at       document-idle
// @noframes
// @license      MIT
// @icon         https://www.google.com/s2/favicons?domain=poki.com
// @author       jonilul
// @homepageURL  https://greasyfork.org/
// @supportURL   https://greasyfork.org/
// ==/UserScript==

(() => {
  'use strict';

  // ---------- tiny utils ----------
  const raf = (fn) => requestAnimationFrame(fn);
  const once = (el, type, fn) => el.addEventListener(type, fn, { once: true });

  // Fire a custom event on SPA URL changes
  const initUrlChangeHook = () => {
    const _push = history.pushState;
    const _replace = history.replaceState;
    const fire = () => window.dispatchEvent(new Event('poki-urlchange'));
    history.pushState = function() { _push.apply(this, arguments); fire(); };
    history.replaceState = function() { _replace.apply(this, arguments); fire(); };
    window.addEventListener('popstate', fire);
  };

  // Find the site’s “report bug” button by its icon (stable: #reportIcon)
  const findReportButton = () => {
    const use = document.querySelector("button use[href='#reportIcon'], button use[xlink\\:href='#reportIcon']");
    return use ? use.closest('button') : null;
  };

  const anyFullscreenButtonExists = () => !!document.querySelector('#fullscreen-button');

  // Create our fullscreen button (clone site look as provided)
  const createFullscreenButton = () => {
    const btn = document.createElement('button');
    btn.type = 'button';
    btn.id = 'fullscreen-button';
    // Classes from examples the site uses
    btn.className = 'HPn_GzeLxs8_4nNebuj1 mDTrvHhilj2xlIvo_kXA phlaiC_iad_lookW5__d';
    btn.setAttribute('aria-label', 'Vollbild');

    const iconDiv = document.createElement('div');
    iconDiv.className = 'tqh57qBcKxMV9EdZQoAb';
    iconDiv.innerHTML = `
      <svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" class="AUcJqk5uLaoXL0jqRGuH">
        <use xlink:href="#enterFullscreenIcon" href="#enterFullscreenIcon"></use>
      </svg>`;
    btn.append(iconDiv);

    const textDiv = document.createElement('div');
    textDiv.className = 'aAJE6r6D5rwwQuTmZqYG';

    const emptySpan = document.createElement('span');
    emptySpan.className = 'L6WSODmebiIqJJOEi46E Vlw13G6cUIC6W9LiGC_X';
    textDiv.append(emptySpan);

    const label = document.createElement('span');
    label.className = 'L6WSODmebiIqJJOEi46E tz2DEu5qBC9Yd6hJGjoW';
    label.textContent = 'Vollbild';
    textDiv.append(label);

    btn.append(textDiv);

    btn.addEventListener('click', () => {
      const elem = document.documentElement;
      if (!document.fullscreenElement) {
        (elem.requestFullscreen || elem.webkitRequestFullscreen || elem.msRequestFullscreen)?.call(elem);
      } else {
        (document.exitFullscreen || document.webkitExitFullscreen || document.msExitFullscreen)?.call(document);
      }
    });

    return btn;
  };

  // Insert right next to the report button (exactly "after" it)
  const insertAfter = (newNode, referenceNode) => {
    referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
  };

  // The lean orchestrator: tries to add only when needed
  let localObserver = null;
  const ensureButton = () => {
    // If site already has fullscreen/minimize button, do nothing
    if (anyFullscreenButtonExists()) return;

    // Try to find the target anchor (report button)
    const reportBtn = findReportButton();
    if (!reportBtn) return;

    // Create and place our button
    const btn = createFullscreenButton();
    insertAfter(btn, reportBtn);

    // Observe ONLY the small control area to restore if the site nukes/changes it
    if (localObserver) localObserver.disconnect();
    localObserver = new MutationObserver(() => {
      // If our button disappeared and site didn't add their own, re-add fast
      if (!anyFullscreenButtonExists()) {
        // Debounce to end of microtask + frame
        localObserver.disconnect();
        Promise.resolve().then(() => raf(ensureButton));
      }
    });
    // Narrow scope: just the parent container where these buttons live
    const scope = reportBtn.parentElement || reportBtn;
    localObserver.observe(scope, { childList: true });

    // Also stop observing if a native fullscreen/minimize button appears
    const stopIfNativeAppears = () => {
      if (anyFullscreenButtonExists() && btn.isConnected && btn !== document.querySelector('#fullscreen-button')) {
        // Our id collides by design, so if a different node with that id exists, remove ours
        btn.remove();
        localObserver?.disconnect();
      }
    };
    once(document, 'fullscreenchange', stopIfNativeAppears);
  };

  // Minimal event hooks (no whole-tree observers):
  // 1) Run on load/idle
  document.addEventListener('readystatechange', () => raf(ensureButton));
  raf(ensureButton);

  // 2) When entering/exiting fullscreen the site often swaps controls
  document.addEventListener('fullscreenchange', () => raf(ensureButton), { passive: true });

  // 3) React to SPA route changes (switching games on poki)
  initUrlChangeHook();
  window.addEventListener('poki-urlchange', () => {
    // Delay a tick to let the new DOM paint
    setTimeout(() => raf(ensureButton), 50);
  }, { passive: true });

  // 4) Very light safety net in case the site hot-swaps without history events
  //    Runs rarely, auto-noops when everything's fine
  setInterval(() => {
    if (!anyFullscreenButtonExists()) ensureButton();
  }, 2500);
})();