您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
一个功能强大的全屏按钮,通过油猴菜单进行配置。支持拖动、自动淡化、可选自动贴边、位置重置。
当前为
// ==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(); })();