// ==UserScript==
// @name 全屏按钮(适用于移动设备)
// @name:en Full screen button (for mobile devices)
// @namespace http://tampermonkey.net/
// @version 3.0
// @description 一个功能强大的全屏按钮,通过油猴菜单进行配置。支持拖动、自动淡化、可选自动贴边、位置重置。
// @description:zh-CN 一个功能强大的全屏按钮,通过油猴菜单进行配置。支持拖动、自动淡化、可选自动贴边、位置重置。
// @description:en A powerful fullscreen button, configurable via the Greasemonkey menu. Supports dragging, auto-fading, optional auto-sticking, and position reset.
// @author 凡留钰 + ChatGPT + Gemini
// @match *://*/*
// @noframes
// @icon https://greasyfork.s3.us-east-2.amazonaws.com/4eb17e88irkc3910fvbpp4f0h270
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @grant GM_getValue
// @grant GM_setValue
// @license MIT
// ==/UserScript==
(function () {
'use strict';
// [优化] 增加 document.body 存在性检查,提高脚本健壮性
if (!document.body) {
console.warn('[Fullscreen Button] Document body not found, script will not run.');
return;
}
// --- 1. 配置中心 ---
const CONFIG = {
initialLeftPercent: 98,
initialTopPercent: 90,
buttonSize: '4vw',
minSize: '24px',
maxSize: '48px',
fadeOpacity: 0.4,
hoverOpacity: 0.8,
fadeDelay: 3000,
snapTransition: 'left 0.3s ease-out, top 0.3s ease-out',
opacityTransition: 'opacity 0.3s ease',
storageKeySnapping: 'enableEdgeSnapping',
};
// --- 2. 状态管理器 ---
const state = {
dragging: false,
moved: false,
hideTimer: null,
animationFrameId: null,
// [优化] 直接从CONFIG初始化,避免值重复书写
leftPercent: CONFIG.initialLeftPercent,
topPercent: CONFIG.initialTopPercent,
menuCommandIds: [],
};
// --- 3. 创建并初始化按钮 ---
const btn = document.createElement('button');
// [优化] 对CSS属性进行逻辑分组,提高可读性
Object.assign(btn.style, {
// 定位与层级
position: 'fixed',
zIndex: '2147483647',
// 尺寸
width: CONFIG.buttonSize,
height: CONFIG.buttonSize,
minWidth: CONFIG.minSize,
minHeight: CONFIG.minSize,
maxWidth: CONFIG.maxSize,
maxHeight: CONFIG.maxSize,
// 外观
backgroundImage: 'url("https://greasyfork.s3.us-east-2.amazonaws.com/4eb17e88irkc3910fvbpp4f0h270")',
backgroundSize: 'cover',
borderRadius: '50%',
boxShadow: '0 2px 8px rgba(0,0,0,0.3)',
border: 'none',
opacity: CONFIG.hoverOpacity,
// 行为与过渡
cursor: 'grab',
userSelect: 'none',
transition: CONFIG.opacityTransition,
});
document.body.appendChild(btn);
// --- 4. 核心功能函数 ---
function updatePosition() { const w = btn.offsetWidth, h = btn.offsetHeight; btn.style.left = `${(state.leftPercent / 100) * (window.innerWidth - w)}px`; btn.style.top = `${(state.topPercent / 100) * (window.innerHeight - h)}px`; }
function clampAndUpdatePosition() { state.leftPercent = Math.max(0, Math.min(100, state.leftPercent)); state.topPercent = Math.max(0, Math.min(100, state.topPercent)); updatePosition(); }
function updateDragPosition(e) { const touch = e.touches ? e.touches[0] : e; const w = btn.offsetWidth, h = btn.offsetHeight; state.leftPercent = (w > 0) ? ((touch.clientX - state.offsetX) / (window.innerWidth - w)) * 100 : 0; state.topPercent = (h > 0) ? ((touch.clientY - state.offsetY) / (window.innerHeight - h)) * 100 : 0; clampAndUpdatePosition(); state.animationFrameId = null; }
function toggleFullscreen() { if (!document.fullscreenElement) document.documentElement.requestFullscreen().catch(err => console.error(`[Fullscreen Button] Error: ${err.message}`)); else document.exitFullscreen(); }
// --- 5. 事件处理逻辑 ---
function dragStart(e) { e.preventDefault(); const touch = e.touches ? e.touches[0] : e; state.dragging = true; state.moved = false; btn.style.transition = CONFIG.opacityTransition; const r = btn.getBoundingClientRect(); state.offsetX = touch.clientX - r.left; state.offsetY = touch.clientY - r.top; state.startX = touch.clientX; state.startY = touch.clientY; btn.style.cursor = 'grabbing'; clearHideTimer(); document.addEventListener('mousemove', dragMove, { passive: false }); document.addEventListener('mouseup', dragEnd, { passive: false }); document.addEventListener('touchmove', dragMove, { passive: false }); document.addEventListener('touchend', dragEnd, { passive: false }); }
function dragMove(e) { if (!state.dragging) return; e.preventDefault(); const touch = e.touches ? e.touches[0] : e; if (!state.moved && (Math.abs(touch.clientX - state.startX) > 5 || Math.abs(touch.clientY - state.startY) > 5)) state.moved = true; if (!state.animationFrameId) state.animationFrameId = requestAnimationFrame(() => updateDragPosition(e)); }
function dragEnd() { if (!state.dragging) return; state.dragging = false; document.removeEventListener('mousemove', dragMove); document.removeEventListener('mouseup', dragEnd); document.removeEventListener('touchmove', dragMove); document.removeEventListener('touchend', dragEnd); if (state.animationFrameId) { cancelAnimationFrame(state.animationFrameId); state.animationFrameId = null; } btn.style.cursor = 'grab'; if (state.moved) { if (GM_getValue(CONFIG.storageKeySnapping, true)) applyEdgeSnapping(); } else { toggleFullscreen(); } startHideTimer(); }
// --- 6. 自动淡化逻辑 ---
function startHideTimer() { clearTimeout(state.hideTimer); state.hideTimer = setTimeout(() => { btn.style.opacity = CONFIG.fadeOpacity; }, CONFIG.fadeDelay); }
function clearHideTimer() { clearTimeout(state.hideTimer); btn.style.opacity = CONFIG.hoverOpacity; }
// --- 7. 与油猴菜单交互的核心函数 ---
function applyEdgeSnapping() { state.leftPercent = state.leftPercent > 50 ? 100 : 0; btn.style.transition = `${CONFIG.opacityTransition}, ${CONFIG.snapTransition}`; clampAndUpdatePosition(); }
function resetButtonPosition() { state.leftPercent = CONFIG.initialLeftPercent; state.topPercent = CONFIG.initialTopPercent; if (GM_getValue(CONFIG.storageKeySnapping, true)) { applyEdgeSnapping(); } else { btn.style.transition = CONFIG.opacityTransition; clampAndUpdatePosition(); } startHideTimer(); /* [优化] 重置后也启动淡出计时器,统一行为 */ }
/**
* [优化] 提取出的切换自动贴边功能的具体实现函数
*/
function toggleSnapping() {
const isEnabled = GM_getValue(CONFIG.storageKeySnapping, true);
const newValue = !isEnabled;
GM_setValue(CONFIG.storageKeySnapping, newValue);
if (newValue) {
applyEdgeSnapping();
} else {
btn.style.transition = CONFIG.opacityTransition;
}
// 关键:再次调用主函数来刷新整个菜单
updateAllMenuCommands();
}
/**
* [结构优化] 统一更新所有油猴菜单命令
*/
function updateAllMenuCommands() {
state.menuCommandIds.forEach(id => GM_unregisterMenuCommand(id));
state.menuCommandIds = [];
// --- 菜单项1:重置按钮位置 ---
const resetId = GM_registerMenuCommand('重置按钮位置', resetButtonPosition);
state.menuCommandIds.push(resetId);
// --- 菜单项2:切换自动贴边 ---
const isSnappingEnabled = GM_getValue(CONFIG.storageKeySnapping, true);
const snappingMenuText = (isSnappingEnabled ? '⬜️ 关闭' : '✅ 开启') + ' 自动贴边';
// [优化] 此处直接调用已命名的回调函数,代码更清晰
const snapId = GM_registerMenuCommand(snappingMenuText, toggleSnapping);
state.menuCommandIds.push(snapId);
}
// --- 8. 初始化与事件绑定 ---
btn.addEventListener('mousedown', dragStart);
btn.addEventListener('touchstart', dragStart, { passive: false });
['mouseenter', 'touchstart'].forEach(evt => btn.addEventListener(evt, clearHideTimer));
['mouseleave', 'touchend'].forEach(evt => btn.addEventListener(evt, startHideTimer));
btn.addEventListener('click', e => { e.preventDefault(); e.stopPropagation(); }, true);
window.addEventListener('resize', clampAndUpdatePosition);
window.addEventListener('orientationchange', clampAndUpdatePosition);
updateAllMenuCommands();
// --- 9. 首次运行 ---
if (GM_getValue(CONFIG.storageKeySnapping, true)) {
applyEdgeSnapping();
} else {
clampAndUpdatePosition();
}
startHideTimer();
})();