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.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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);
})();