Buttons for quick scrolling in different directions.
当前为
// ==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);
});
})();