Force Video Fullscreen for Bonga

Smart fullscreen with auto-fit for BongaCams and mirrors. Detects black borders in video frames and switches between contain/cover modes. Supports manual override.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey, Greasemonkey или Violentmonkey.

Вам потребуется установить расширение, например Tampermonkey или Violentmonkey, чтобы установить этот скрипт.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Userscripts.

Чтобы установить этот скрипт, сначала вы должны установить расширение браузера, например Tampermonkey.

Чтобы установить этот скрипт, вы должны установить расширение — менеджер скриптов.

(у меня уже есть менеджер скриптов, дайте мне установить скрипт!)

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

(у меня уже есть менеджер стилей, дайте мне установить скрипт!)

// ==UserScript==
// @name         Force Video Fullscreen for Bonga
// @name:ru      Полноэкранный режим для Bonga
// @namespace    qwen-helper
// @version      3.1
// @description  Smart fullscreen with auto-fit for BongaCams and mirrors. Detects black borders in video frames and switches between contain/cover modes. Supports manual override.
// @description:ru  Умный полноэкранный режим для BongaCams и зеркал. Определяет чёрные бордюры в кадре и автоматически переключает режимы contain/cover. Поддерживает ручное переключение.
// @author       QwenWhipped
// @include      /^https?:\/\/.*bonga.*/
// @run-at       document-idle
// @license      MIT
// @grant        none
// ==/UserScript==

(function () {
  'use strict';

  // ===== НАСТРОЙКИ =====
  const CONFIG = {
    activateDomain: 'bonga',  // подстрока в домене для активации
    debug: false,              // true — логи в консоль, false — тихо
    toastDuration: 1800,      // длительность toast-сообщений (мс)
  };

  const log = CONFIG.debug ? (...args) => console.log('[FS Script]', ...args) : () => {};

  // Скрипт активируется только если в домене есть подстрока из CONFIG
  if (!location.hostname.includes(CONFIG.activateDomain)) return;

  log('Запуск скрипта на', location.hostname);

  let manualMode = null;
  const detectCache = new WeakMap();

  function injectCss() {
    if (document.getElementById('__fsSmartFix__')) return;
    const style = document.createElement('style');
    style.id = '__fsSmartFix__';
    style.textContent = `
      video:fullscreen,
      video:-webkit-full-screen {
        background: #000 !important;
        width: 100% !important;
        height: 100% !important;
        margin: 0 !important;
        padding: 0 !important;
        border: none !important;
        max-width: 100vw !important;
        max-height: 100vh !important;
      }

      #__fsBtn__ {
        transition: background-color 0.3s ease, transform 0.1s ease;
      }

      #__fsBtn__:hover {
        background: rgba(0, 0, 0, 0.9) !important;
        transform: scale(1.05);
      }

      #__fsBtn__.flash {
        background: #4CAF50 !important;
      }
    `;
    (document.head || document.documentElement).appendChild(style);
  }

  function analyzeFrame(video) {
    try {
      const w = 160, h = 90;
      const canvas = document.createElement('canvas');
      canvas.width = w;
      canvas.height = h;
      const ctx = canvas.getContext('2d', { willReadFrequently: true });
      ctx.drawImage(video, 0, 0, w, h);
      const data = ctx.getImageData(0, 0, w, h).data;

      const colSharp = [];
      for (let x = 1; x < w; x++) {
        let sum = 0, cnt = 0;
        for (let y = 0; y < h; y += 2) {
          const i = (y * w + x) * 4;
          const p = (y * w + x - 1) * 4;
          sum += Math.abs(data[i] - data[p]) +
                 Math.abs(data[i + 1] - data[p + 1]) +
                 Math.abs(data[i + 2] - data[p + 2]);
          cnt++;
        }
        colSharp.push(sum / cnt);
      }

      const avg = (arr) => arr.length ? arr.reduce((a, b) => a + b, 0) / arr.length : 0;
      const left   = avg(colSharp.slice(0, Math.floor(w * 0.20)));
      const right  = avg(colSharp.slice(Math.floor(w * 0.80)));
      const center = avg(colSharp.slice(Math.floor(w * 0.35), Math.floor(w * 0.65)));

      if (center <= 3) return 'dark';
      if (center > left * 2.2 && center > right * 2.2) return 'borders';
      return 'clean';
    } catch (e) {
      return 'cors_blocked';
    }
  }

  function autoDetectMode(video) {
    if (!video) return 'contain';
    if (detectCache.has(video)) return detectCache.get(video);

    const frame = analyzeFrame(video);
    const vw = video.videoWidth || 0;
    const vh = video.videoHeight || 0;
    const streamAR = vw && vh ? vw / vh : 0;

    let mode = 'contain';
    let reason = streamAR && streamAR < 0.8 ? 'вертикальное видео' : 'горизонтальное видео';

    if (frame === 'borders') {
      mode = 'cover';
      reason = 'найдены бордюры';
    } else if (frame === 'cors_blocked') {
      reason = 'CORS блок (fallback)';
    }

    detectCache.set(video, mode);
    video.__fsReason = reason;

    if (!video.__fsResizeAttached) {
      let resizeTimer = null;
      video.addEventListener('resize', () => {
        clearTimeout(resizeTimer);
        resizeTimer = setTimeout(() => {
          detectCache.delete(video);
          const active = document.fullscreenElement || document.webkitFullscreenElement;
          if (active === video || (active && active.contains && active.contains(video))) {
            applyFitMode(video, getMode(video));
          }
        }, 200);
      });
      video.__fsResizeAttached = true;
    }

    return mode;
  }

  function getMode(video) {
    return manualMode || autoDetectMode(video);
  }

  function applyFitMode(video, mode) {
    if (!video) return;
    if (!video.__fsOrigSaved) {
      video.__fsOrigFit = video.style.objectFit || '';
      video.__fsOrigPos = video.style.objectPosition || '';
      video.__fsOrigSaved = true;
    }
    video.style.setProperty('object-fit', mode, 'important');
    video.style.setProperty('object-position', 'center center', 'important');
  }

  function resetVideoStyles(video) {
    if (!video) return;
    if (video.__fsOrigSaved) {
      if (video.__fsOrigFit) {
        video.style.setProperty('object-fit', video.__fsOrigFit, 'important');
      } else {
        video.style.removeProperty('object-fit');
      }
      if (video.__fsOrigPos) {
        video.style.setProperty('object-position', video.__fsOrigPos, 'important');
      } else {
        video.style.removeProperty('object-position');
      }
      video.__fsOrigSaved = false;
    } else {
      video.style.removeProperty('object-fit');
      video.style.removeProperty('object-position');
    }
  }

  function showToast(text) {
    let toast = document.getElementById('__fsToast__');
    if (!toast) {
      toast = document.createElement('div');
      toast.id = '__fsToast__';
      toast.style.cssText = 'position:fixed;top:20px;left:50%;transform:translateX(-50%);z-index:2147483647;background:rgba(0,0,0,.8);color:#fff;padding:8px 16px;border-radius:8px;font-size:13px;pointer-events:none;opacity:0;transition:opacity .3s;';
      (document.body || document.documentElement).appendChild(toast);
    }
    toast.textContent = text;
    toast.style.opacity = '1';
    clearTimeout(toast.__hideTimer);
    toast.__hideTimer = setTimeout(() => { toast.style.opacity = '0'; }, CONFIG.toastDuration);
  }

  function flashButton() {
    const btn = document.getElementById('__fsBtn__');
    if (!btn) return;
    btn.classList.add('flash');
    setTimeout(() => btn.classList.remove('flash'), 300);
  }

  function getAllVideos() {
    try { return Array.from(document.querySelectorAll('video')); }
    catch (e) { return []; }
  }

  function getLargestVideo() {
    const videos = getAllVideos();
    if (!videos.length) return null;
    return videos.sort((a, b) => {
      const areaA = (a.videoWidth || a.clientWidth || 0) * (a.videoHeight || a.clientHeight || 0);
      const areaB = (b.videoWidth || b.clientWidth || 0) * (b.videoHeight || b.clientHeight || 0);
      return areaB - areaA;
    })[0];
  }

  function requestNativeFullscreen(video) {
    if (!video) return;
    video.controls = true;
    applyFitMode(video, getMode(video));

    try {
      const fn = video.requestFullscreen || video.webkitRequestFullscreen;
      if (fn) {
        const p = fn.call(video);
        if (p && p.catch) p.catch(() => {
          if (video.parentElement) {
            const pFn = video.parentElement.requestFullscreen || video.parentElement.webkitRequestFullscreen;
            if (pFn) pFn.call(video.parentElement);
          }
        });
      }
    } catch (err) {
      log('Fullscreen error:', err);
    }
  }

  function activateFullscreen() {
    const video = getLargestVideo();
    if (!video) return;
    requestNativeFullscreen(video);
    const mode = getMode(video);
    showToast('FS: ' + mode + (manualMode ? ' (ручной)' : ' (авто: ' + (video.__fsReason || '') + ')'));
  }

  function updateBtnText() {
    const btn = document.getElementById('__fsBtn__');
    if (!btn) return;
    const modeLabel = manualMode ? manualMode : 'авто';
    btn.textContent = `FS [${modeLabel}]`;
  }

  function toggleManualMode() {
    const active = document.fullscreenElement || document.webkitFullscreenElement;
    const video = (active && active.tagName === 'VIDEO') ? active : getLargestVideo();

    if (manualMode === null) {
      manualMode = 'cover';
    } else if (manualMode === 'cover') {
      manualMode = 'contain';
    } else {
      manualMode = null;
    }

    if (video) {
      if (manualMode === null) detectCache.delete(video);
      applyFitMode(video, getMode(video));
    }

    updateBtnText();
    flashButton();
    showToast('Режим: ' + (manualMode ? manualMode + ' (ручной)' : 'авто'));
  }

  window.addEventListener('keydown', (e) => {
    if (e.altKey && e.shiftKey && e.code === 'KeyF') {
      e.preventDefault();
      e.stopPropagation();
      activateFullscreen();
    }

    if (e.altKey && e.shiftKey && e.code === 'KeyC') {
      e.preventDefault();
      e.stopPropagation();
      toggleManualMode();
    }

    if (e.altKey && e.shiftKey && e.code === 'KeyA') {
      e.preventDefault();
      e.stopPropagation();
      manualMode = null;
      const active = document.fullscreenElement || document.webkitFullscreenElement;
      const video = (active && active.tagName === 'VIDEO') ? active : getLargestVideo();
      if (video) {
        detectCache.delete(video);
        applyFitMode(video, autoDetectMode(video));
      }
      updateBtnText();
      flashButton();
      showToast('Режим: авто');
    }
  }, true);

  ['fullscreenchange', 'webkitfullscreenchange'].forEach(evt => {
    document.addEventListener(evt, () => {
      const el = document.fullscreenElement || document.webkitFullscreenElement;

      if (el) {
        let video = null;
        if (el.tagName === 'VIDEO') {
          video = el;
        } else if (el.querySelector) {
          video = el.querySelector('video');
        }
        if (video) {
          applyFitMode(video, getMode(video));
        }
      } else {
        getAllVideos().forEach(video => {
          resetVideoStyles(video);
          detectCache.delete(video);
        });
      }
    });
  });

  function addButton() {
    if (document.getElementById('__fsBtn__')) return;
    const btn = document.createElement('button');
    btn.id = '__fsBtn__';
    btn.title = 'ЛКМ: полноэкранный режим | ПКМ: смена режима\nХоткеи: Alt+Shift+F (FS), Alt+Shift+C (Смена), Alt+Shift+A (Авто)';
    btn.style.cssText = 'position:fixed;right:14px;bottom:14px;z-index:2147483647;background:rgba(0,0,0,.75);color:#fff;border:none;border-radius:8px;padding:8px 12px;font-size:13px;cursor:pointer;user-select:none;font-weight:bold;box-shadow:0 2px 8px rgba(0,0,0,0.5);';

    btn.addEventListener('click', activateFullscreen);

    btn.addEventListener('contextmenu', (e) => {
      e.preventDefault();
      e.stopPropagation();
      toggleManualMode();
    });

    (document.body || document.documentElement).appendChild(btn);
    updateBtnText();
  }

  function checkForVideo() {
    if (getLargestVideo()) {
      addButton();
    }
  }

  injectCss();

  let debouncedTimer = null;
  const obs = new MutationObserver(() => {
    if (document.getElementById('__fsBtn__')) return;
    clearTimeout(debouncedTimer);
    debouncedTimer = setTimeout(checkForVideo, 300);
  });

  obs.observe(document.documentElement, { childList: true, subtree: true });
  checkForVideo();

  log('Скрипт инициализирован успешно');
})();