TesterTV_ScrollButtons

Buttons for quick scrolling in different directions.

Verze ze dne 30. 08. 2025. Zobrazit nejnovější verzi.

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

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

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

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

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

// ==UserScript==
// @name         TesterTV_ScrollButtons
// @namespace    https://greasyfork.org/ru/scripts/482232-testertv-scrollbuttons
// @version      2025.08.30
// @description  Buttons for quick scrolling in different directions.
// @license      GPL-3.0-or-later
// @author       TesterTV
// @match        *://*/*
// @grant        GM_openInTab
// @grant        GM.setValue
// @grant        GM.getValue
// ==/UserScript==

(async function () {
  'use strict';

  const isIframe = window !== window.top;

  // ====== Constants / Defaults
  const BTN_SIZE = '66px';
  const FONT_SIZE = '50px';
  const GAP_SIDE = '0px';
  const GAP_BOTTOM = '0px';
  const DEF_SIDE_OPT = '0';  // 0=right,1=left,2=disable
  const DEF_BOTTOM_OPT = '0'; // 0=enable,1=disable
  const DEF_SIDE_SLIDER = '42'; // %
  const DEF_BOTTOM_SLIDER = '50'; // %
  const Z_TOPSIDE = '9996';
  const Z_BOTHSIDE = '9998';
  const Z_BOTTOM = '9999';
  const Z_PANEL = '10000';

  // ====== Styles
  const css = `
    .scroll-btn {
      height:${BTN_SIZE}; width:${BTN_SIZE}; font-size:${FONT_SIZE};
      position:fixed; background:transparent; border:none; outline:none;
      opacity:.05; cursor:pointer; user-select:none;
    }
    .scroll-btn:hover { opacity:1 }
    #TopSideButton { z-index:${Z_TOPSIDE}; }
    #BottomSideButton { z-index:${Z_BOTHSIDE}; }
    #BottomButton { z-index:${Z_BOTTOM}; transform:translate(-50%, 0); }
    #OptionPanel {
      position:fixed; top:50%; left:50%; transform:translate(-50%, -50%);
      background:#303236; color:#fff; padding:10px; border:1px solid grey;
      z-index:${Z_PANEL}; font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;
    }
    #OptionPanel .title { font-weight:bold; text-decoration:underline; font-size:20px; margin-bottom:6px; display:block; }
    #OptionPanel .row { margin:6px 0; }
    #OptionPanel label { font-size:15px; margin-right:6px; }
    #OptionPanel select { font-size:15px; }
    #OptionPanel input[type=range] {
      width:100px; background:#74e3ff; border:none; height:5px; outline:none; appearance:none;
    }
    #DonateBtn {
      width:180px; height:25px; font-size:10px; color:#303236; background:#fff;
      border:1px solid grey; position:relative; left:50%; transform:translateX(-50%); margin-top:8px;
    }
  `;
  const style = document.createElement('style');
  style.textContent = css;
  document.documentElement.appendChild(style);

  // ====== Helpers
  const makeBtn = (id, text, onClick) => {
    const b = document.createElement('button');
    b.id = id;
    b.className = 'scroll-btn';
    b.textContent = text;
    document.body.appendChild(b);
    b.addEventListener('click', onClick);
    if (isIframe) b.style.display = 'none';
    return b;
  };

  const scrollToTop = () => window.scrollTo({ top: 0, behavior: 'auto' });
  const scrollToBottom = () => window.scrollTo({ top: document.body.scrollHeight, behavior: 'auto' });

  const clamp = (v, min, max) => Math.max(min, Math.min(max, v));

  // ====== Create Buttons
  const TopSideButton = makeBtn('TopSideButton', '⬆️', scrollToTop);
  const BottomSideButton = makeBtn('BottomSideButton', '⬇️', scrollToBottom);
  const BottomButton = makeBtn('BottomButton', '⬆️', scrollToTop);

  // ====== Load settings
  let [sideOpt, sideSlider, bottomOpt, bottomSlider] = await Promise.all([
    GM.getValue('SideButtonsOption', DEF_SIDE_OPT),
    GM.getValue('SideButtonsSliderOption', DEF_SIDE_SLIDER),
    GM.getValue('BottomButtonOption', DEF_BOTTOM_OPT),
    GM.getValue('BottomButtonSliderOption', DEF_BOTTOM_SLIDER),
  ]);

  // Coerce to strings
  sideOpt = String(sideOpt || DEF_SIDE_OPT);
  bottomOpt = String(bottomOpt || DEF_BOTTOM_OPT);
  sideSlider = String(sideSlider || DEF_SIDE_SLIDER);
  bottomSlider = String(bottomSlider || DEF_BOTTOM_SLIDER);

  // ====== Apply settings to UI
  function applySideButtons() {
    if (isIframe) {
      TopSideButton.style.display = 'none';
      BottomSideButton.style.display = 'none';
      return;
    }

    const disabled = sideOpt === '2';
    TopSideButton.style.display = disabled ? 'none' : 'block';
    BottomSideButton.style.display = disabled ? 'none' : 'block';

    if (disabled) return;

    // Vertical positions
    const v = clamp(Number(sideSlider), 4, 96);
    TopSideButton.style.top = v + '%';
    BottomSideButton.style.top = (100 - v) + '%';

    // Horizontal positions
    if (sideOpt === '1') {
      TopSideButton.style.left = GAP_SIDE;
      BottomSideButton.style.left = GAP_SIDE;
      TopSideButton.style.right = '';
      BottomSideButton.style.right = '';
    } else {
      TopSideButton.style.right = GAP_SIDE;
      BottomSideButton.style.right = GAP_SIDE;
      TopSideButton.style.left = '';
      BottomSideButton.style.left = '';
    }
  }

  function applyBottomButton() {
    if (isIframe) {
      BottomButton.style.display = 'none';
      return;
    }
    const enabled = bottomOpt !== '1';
    BottomButton.style.display = enabled ? 'block' : 'none';
    BottomButton.style.bottom = GAP_BOTTOM;

    // Horizontal % with center correction
    const x = clamp(Number(bottomSlider), 0, 95);
    BottomButton.style.left = x + '%';
  }

  function updateButtonsForMediaOrFullscreen() {
    const isMediaUrl = /\.(?:jpg|jpeg|png|gif|svg|webp|apng|webm|mp4|mp3|wav|ogg)(?:[#?].*)?$/i
      .test(location.pathname + location.search + location.hash);
    const isFullScreen = !!(document.fullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement || document.msFullscreenElement);
    const vis = (isMediaUrl || isFullScreen) ? 'hidden' : 'visible';
    [TopSideButton, BottomSideButton, BottomButton].forEach(b => b.style.visibility = vis);
  }

  applySideButtons();
  applyBottomButton();
  updateButtonsForMediaOrFullscreen();

  // Keep in sync on zoom/resize and fullscreen
  window.addEventListener('resize', () => {
    applySideButtons();
    applyBottomButton();
  });
  ['fullscreenchange', 'webkitfullscreenchange', 'mozfullscreenchange', 'MSFullscreenChange']
    .forEach(evt => document.addEventListener(evt, updateButtonsForMediaOrFullscreen));
  setInterval(updateButtonsForMediaOrFullscreen, 1000); // keep legacy polling

  // ====== Options Panel
  function showOptionsPanel(e) {
    e.preventDefault();

    // Remove any existing panel
    const existing = document.getElementById('OptionPanel');
    if (existing) existing.remove();

    const panel = document.createElement('div');
    panel.id = 'OptionPanel';
    panel.innerHTML = `
      <span class="title">Options</span>
      <div class="row">
        <label>Side buttons:</label>
        <select id="SidePos">
          <option value="0">Right</option>
          <option value="1">Left</option>
          <option value="2">Disable</option>
        </select>
      </div>
      <div class="row">
        <label>Bottom button:</label>
        <select id="BottomVis">
          <option value="0">Enable</option>
          <option value="1">Disable</option>
        </select>
      </div>
      <div class="row">
        <label>⬆️⬇️ position:</label>
        <input id="SideV" type="range" min="4" max="96" step="1">
      </div>
      <div class="row">
        <label>⬆️ position:</label>
        <input id="BottomH" type="range" min="0" max="95" step="1">
      </div>
      <button id="DonateBtn">💳Please support me!🤗</button>
    `;
    document.body.appendChild(panel);

    // Init fields
    const ddSide = panel.querySelector('#SidePos');
    const ddBottom = panel.querySelector('#BottomVis');
    const rngSide = panel.querySelector('#SideV');
    const rngBottom = panel.querySelector('#BottomH');

    ddSide.value = sideOpt;
    ddBottom.value = bottomOpt;
    rngSide.value = clamp(Number(sideSlider), 4, 96);
    rngBottom.value = clamp(Number(bottomSlider), 0, 95);

    // Enforce "at least one visible"
    function wouldHideAll(nextSideOpt = ddSide.value, nextBottomOpt = ddBottom.value) {
      return nextSideOpt === '2' && nextBottomOpt === '1';
    }

    ddSide.addEventListener('change', async () => {
      const next = ddSide.value;
      if (wouldHideAll(next, ddBottom.value)) {
        alert("All buttons can't be invisible! 🫠");
        ddSide.value = sideOpt;
        return;
      }
      sideOpt = next;
      await GM.setValue('SideButtonsOption', sideOpt);
      applySideButtons();
    });

    ddBottom.addEventListener('change', async () => {
      const next = ddBottom.value;
      if (wouldHideAll(ddSide.value, next)) {
        alert("All buttons can't be invisible! 🫠");
        ddBottom.value = bottomOpt;
        return;
      }
      bottomOpt = next;
      await GM.setValue('BottomButtonOption', bottomOpt);
      applyBottomButton();
    });

    rngSide.addEventListener('input', async () => {
      sideSlider = String(rngSide.value);
      await GM.setValue('SideButtonsSliderOption', sideSlider);
      applySideButtons();
    });

    rngBottom.addEventListener('input', async () => {
      bottomSlider = String(rngBottom.value);
      await GM.setValue('BottomButtonSliderOption', bottomSlider);
      applyBottomButton();
    });

    panel.querySelector('#DonateBtn').addEventListener('click', () => {
      GM_openInTab('https://...');
    });

    // Close panel when clicking outside
    const onDocClick = (evt) => {
      if (!panel.contains(evt.target)) {
        panel.remove();
        document.removeEventListener('click', onDocClick, true);
      }
    };
    setTimeout(() => document.addEventListener('click', onDocClick, true), 0);
  }

  // Open panel on right-click of any button
  [TopSideButton, BottomSideButton, BottomButton].forEach(b => {
    b.addEventListener('contextmenu', showOptionsPanel);
  });

})();