全屏按钮(适用于移动设备)

一个功能强大的全屏按钮,通过油猴菜单进行配置。支持拖动、自动淡化、可选自动贴边、位置重置。

当前为 2025-06-29 提交的版本,查看 最新版本

// ==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();

})();