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