Greasy Fork

来自缓存

Greasy Fork is available in English.

Reddit中文翻译助手

为Reddit页面添加中文翻译功能,支持响应式适配

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Reddit中文翻译助手
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  为Reddit页面添加中文翻译功能,支持响应式适配
// @author       djzhao
// @match        https://www.reddit.com/*
// @match        https://old.reddit.com/*
// @grant        none
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    let currentElement = null; // 当前显示的元素(按钮或指示器)

    // 检查当前URL是否已经包含中文翻译参数
    function hasChineseTranslation() {
        const urlParams = new URLSearchParams(window.location.search);
        return urlParams.get('tl') === 'zh-hans';
    }

    // 约束元素位置在视窗范围内
    function constrainToViewport(element) {
        if (!element) return;

        const rect = element.getBoundingClientRect();
        const viewportWidth = window.innerWidth;
        const viewportHeight = window.innerHeight;

        let newLeft = rect.left;
        let newTop = rect.top;
        let needsUpdate = false;

        // 检查右边界
        if (rect.right > viewportWidth) {
            newLeft = viewportWidth - element.offsetWidth - 10; // 留10px边距
            needsUpdate = true;
        }

        // 检查左边界
        if (rect.left < 0) {
            newLeft = 10; // 留10px边距
            needsUpdate = true;
        }

        // 检查下边界
        if (rect.bottom > viewportHeight) {
            newTop = viewportHeight - element.offsetHeight - 10; // 留10px边距
            needsUpdate = true;
        }

        // 检查上边界
        if (rect.top < 0) {
            newTop = 10; // 留10px边距
            needsUpdate = true;
        }

        // 如果需要更新位置
        if (needsUpdate) {
            element.style.left = newLeft + 'px';
            element.style.top = newTop + 'px';
            element.style.right = 'auto';
            element.style.bottom = 'auto';
        }
    }

    // 处理窗口大小变化
    function handleResize() {
        // 添加防抖处理,避免频繁调用
        clearTimeout(handleResize.timeout);
        handleResize.timeout = setTimeout(() => {
            constrainToViewport(currentElement);
        }, 100);
    }

    // 创建可拖拽功能
    function makeDraggable(element, onClickCallback) {
        let isDragging = false;
        let hasMoved = false;
        let startPos = { x: 0, y: 0 };
        let elementPos = { x: 0, y: 0 };

        element.addEventListener('mousedown', function(e) {
            isDragging = true;
            hasMoved = false;
            startPos.x = e.clientX;
            startPos.y = e.clientY;

            const rect = element.getBoundingClientRect();
            elementPos.x = rect.left;
            elementPos.y = rect.top;

            element.style.cursor = 'grabbing';
            element.style.transition = 'none'; // 禁用过渡动画以提高跟手性
            e.preventDefault();
        });

        document.addEventListener('mousemove', function(e) {
            if (isDragging) {
                const deltaX = e.clientX - startPos.x;
                const deltaY = e.clientY - startPos.y;

                // 如果移动距离超过5px,认为是拖拽操作
                if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) {
                    hasMoved = true;
                }

                const newX = elementPos.x + deltaX;
                const newY = elementPos.y + deltaY;

                // 限制按钮在视窗范围内,留出边距
                const margin = 10;
                const maxX = window.innerWidth - element.offsetWidth - margin;
                const maxY = window.innerHeight - element.offsetHeight - margin;

                const constrainedX = Math.max(margin, Math.min(newX, maxX));
                const constrainedY = Math.max(margin, Math.min(newY, maxY));

                element.style.left = constrainedX + 'px';
                element.style.top = constrainedY + 'px';
                element.style.right = 'auto';
                element.style.bottom = 'auto';
            }
        });

        document.addEventListener('mouseup', function() {
            if (isDragging) {
                isDragging = false;
                element.style.cursor = 'pointer';
                element.style.transition = 'all 0.3s ease'; // 恢复过渡动画

                // 如果没有移动,则触发点击事件
                if (!hasMoved && onClickCallback) {
                    onClickCallback();
                }
            }
        });

        // 触摸设备支持
        element.addEventListener('touchstart', function(e) {
            isDragging = true;
            hasMoved = false;
            const touch = e.touches[0];
            startPos.x = touch.clientX;
            startPos.y = touch.clientY;

            const rect = element.getBoundingClientRect();
            elementPos.x = rect.left;
            elementPos.y = rect.top;

            element.style.transition = 'none';
            e.preventDefault();
        });

        document.addEventListener('touchmove', function(e) {
            if (isDragging) {
                const touch = e.touches[0];
                const deltaX = touch.clientX - startPos.x;
                const deltaY = touch.clientY - startPos.y;

                if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) {
                    hasMoved = true;
                }

                const newX = elementPos.x + deltaX;
                const newY = elementPos.y + deltaY;

                const margin = 10;
                const maxX = window.innerWidth - element.offsetWidth - margin;
                const maxY = window.innerHeight - element.offsetHeight - margin;

                const constrainedX = Math.max(margin, Math.min(newX, maxX));
                const constrainedY = Math.max(margin, Math.min(newY, maxY));

                element.style.left = constrainedX + 'px';
                element.style.top = constrainedY + 'px';
                element.style.right = 'auto';
                element.style.bottom = 'auto';

                e.preventDefault();
            }
        });

        document.addEventListener('touchend', function() {
            if (isDragging) {
                isDragging = false;
                element.style.transition = 'all 0.3s ease';

                if (!hasMoved && onClickCallback) {
                    onClickCallback();
                }
            }
        });
    }

    // 创建悬浮按钮
    function createFloatingButton() {
        const button = document.createElement('div');
        button.id = 'reddit-translate-btn';
        button.innerHTML = '中文';
        button.style.cssText = `
            position: fixed;
            top: 64px;
            right: 16px;
            width: 32px;
            height: 32px;
            background: #ff4500;
            color: white;
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            cursor: pointer;
            font-size: 10px;
            font-weight: bold;
            box-shadow: 0 2px 10px rgba(0,0,0,0.3);
            z-index: 10000;
            transition: all 0.3s ease;
            user-select: none;
            touch-action: none;
        `;

        // 响应式字体大小
        function updateButtonSize() {
            const viewportWidth = window.innerWidth;
            if (viewportWidth < 768) {
                // 移动设备
                button.style.width = '30px';
                button.style.height = '30px';
                button.style.fontSize = '10px';
            } else {
                // 桌面设备
                button.style.width = '32px';
                button.style.height = '32px';
                button.style.fontSize = '10px';
            }
        }

        updateButtonSize();

        // 鼠标悬停效果
        button.addEventListener('mouseenter', function() {
            this.style.transform = 'scale(1.1)';
            this.style.boxShadow = '0 4px 15px rgba(0,0,0,0.4)';
        });

        button.addEventListener('mouseleave', function() {
            this.style.transform = 'scale(1)';
            this.style.boxShadow = '0 2px 10px rgba(0,0,0,0.3)';
        });

        // 添加拖拽功能和点击事件
        makeDraggable(button, function() {
            const currentUrl = new URL(window.location.href);
            currentUrl.searchParams.set('tl', 'zh-hans');
            window.location.href = currentUrl.toString();
        });

        // 监听窗口大小变化以更新按钮尺寸
        const resizeObserver = new ResizeObserver(updateButtonSize);
        resizeObserver.observe(document.documentElement);

        return button;
    }

    // 创建已翻译状态的指示器
    function createTranslatedIndicator() {
        const indicator = document.createElement('div');
        indicator.id = 'reddit-translated-indicator';
        indicator.innerHTML = '已翻译';
        indicator.style.cssText = `
            position: fixed;
            top: 64px;
            right: 16px;
            padding: 4px 8px;
            background: rgba(40, 167, 69, 0.8);
            color: white;
            border-radius: 12px;
            font-size: 10px;
            font-weight: bold;
            box-shadow: 0 1px 5px rgba(0,0,0,0.2);
            z-index: 10000;
            user-select: none;
            cursor: pointer;
            transition: all 0.3s ease;
            opacity: 0.7;
            touch-action: none;
        `;

        // 响应式字体大小
        function updateIndicatorSize() {
            const viewportWidth = window.innerWidth;
            if (viewportWidth < 768) {
                // 移动设备
                indicator.style.fontSize = '9px';
                indicator.style.padding = '3px 6px';
            } else {
                // 桌面设备
                indicator.style.fontSize = '10px';
                indicator.style.padding = '4px 8px';
            }
        }

        updateIndicatorSize();

        // 鼠标悬停效果
        indicator.addEventListener('mouseenter', function() {
            this.style.opacity = '1';
            this.style.transform = 'scale(1.05)';
        });

        indicator.addEventListener('mouseleave', function() {
            this.style.opacity = '0.7';
            this.style.transform = 'scale(1)';
        });

        // 添加拖拽功能和点击事件(点击移除翻译)
        makeDraggable(indicator, function() {
            const currentUrl = new URL(window.location.href);
            currentUrl.searchParams.delete('tl');
            window.location.href = currentUrl.toString();
        });

        // 监听窗口大小变化以更新指示器尺寸
        const resizeObserver = new ResizeObserver(updateIndicatorSize);
        resizeObserver.observe(document.documentElement);

        return indicator;
    }

    // 初始化脚本
    function init() {
        // 等待页面加载完成
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', init);
            return;
        }

        // 移除已存在的按钮或指示器
        const existingBtn = document.getElementById('reddit-translate-btn');
        const existingIndicator = document.getElementById('reddit-translated-indicator');
        if (existingBtn) existingBtn.remove();
        if (existingIndicator) existingIndicator.remove();

        // 检查是否已经是中文翻译状态
        if (hasChineseTranslation()) {
            // 显示已翻译指示器
            currentElement = createTranslatedIndicator();
            document.body.appendChild(currentElement);
        } else {
            // 显示翻译按钮
            currentElement = createFloatingButton();
            document.body.appendChild(currentElement);
        }
    }

    // 监听窗口大小变化
    window.addEventListener('resize', handleResize);

    // 监听设备方向变化(移动设备)
    window.addEventListener('orientationchange', function() {
        setTimeout(() => {
            constrainToViewport(currentElement);
        }, 300); // 等待方向变化完成
    });

    // 监听URL变化(适用于单页应用)
    let lastUrl = location.href;
    new MutationObserver(() => {
        const url = location.href;
        if (url !== lastUrl) {
            lastUrl = url;
            setTimeout(init, 500); // 延迟执行以确保页面更新完成
        }
    }).observe(document, { subtree: true, childList: true });

    // 初始化
    init();
})();