// ==UserScript==
// @name 粤语划词翻译
// @namespace http://tampermonkey.net/
// @version 1.0
// @description 快捷粤语翻译查询
// @author 口吃者
// @match http://*/*
// @include https://*/*
// @include file:///*
// @icon https://www.google.com/s2/favicons?sz=64&domain=shyyp.net
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM.setValue
// @grant GM.getValue
// @license MIT
// ==/UserScript==
const shyypTokenUrl = 'https://shyyp.net/api/gqgq2';
const shyypLongScriptUrl = 'https://shyyp.net/romanizer';//长文注音
const shyypConvertUrl = 'https://shyyp.net/translator';//粤普转换
const shyypSingleWorldUrl = 'https://shyyp.net/w/';
const shyypIconUrl = 'https://shyyp.net/imgs/sheep.svg'
const shyypIconMosaicUrl = 'https://pic2.imge.cc/2024/10/15/670e346fa2205.png'
const shyypIconOnlyHeadUrl = 'https://pic2.imge.cc/2024/10/15/670e3464ab2d1.png'
let textEncry = '';
let selected;// 当前选中文本
let pageX;// 图标显示的 X 坐标
let pageY;// 图标显示的 Y 坐标
const dragFluctuation = 4;// 当拖动多少像素以上时不触发查询
const zIndex = '2147473647'; // 渲染图层
/**鼠标拖动*/
class Drag {
constructor(element) {
this.dragging = false;
this.startDragTime = 0;
this.stopDragTime = 0;
this.mouseDownPositionX = 0;
this.mouseDownPositionY = 0;
this.elementOriginalLeft = parseInt(element.style.left);
this.elementOriginalTop = parseInt(element.style.top);
this.backAndForthLeftMax = 0;
this.backAndForthTopMax = 0;
this.element = element;
// 绑定事件处理函数
// 事件处理函数由dom元素调用,一般是指向dom元素,强制绑定到Drag类上
this.startDrag = this.startDrag.bind(this);
this.dragElement = this.dragElement.bind(this);
this.stopDrag = this.stopDrag.bind(this);
// 添加鼠标事件监听器
this.attachEventListeners();
}
attachEventListeners() {
this.element.addEventListener('mousedown', this.startDrag);
}
detachEventListeners() {
window.removeEventListener('mousemove', this.dragElement);
window.removeEventListener('mouseup', this.stopDrag);
}
startDrag(e) {
//阻止默认鼠标事件,比如选中文字
e.preventDefault();
this.dragging = true;
this.startDragTime = new Date().getTime();
this.mouseDownPositionX = e.clientX;
this.mouseDownPositionY = e.clientY;
this.elementOriginalLeft = parseInt(this.element.style.left);
this.elementOriginalTop = parseInt(this.element.style.top);
this.backAndForthLeftMax = 0;
this.backAndForthTopMax = 0;
// 设置全局鼠标事件
window.addEventListener('mousemove', this.dragElement);
window.addEventListener('mouseup', this.stopDrag);
log('startDrag');
}
stopDrag(e) {
e.preventDefault();
this.dragging = false;
this.stopDragTime = new Date().getTime();
this.detachEventListeners();
log('stopDrag');
}
dragElement(e) {
log('dragging');
if (!this.dragging) {
return;
}
e.preventDefault();
// 移动元素
this.element.style.left = `${this.elementOriginalLeft + (e.clientX - this.mouseDownPositionX)}px`;
this.element.style.top = `${this.elementOriginalTop + (e.clientY - this.mouseDownPositionY)}px`;
// 获取最大移动距离
let left = Math.abs(this.elementOriginalLeft - parseInt(this.element.style.left));
let top = Math.abs(this.elementOriginalTop - parseInt(this.element.style.top));
//更新最大移动距离
if (left > this.backAndForthLeftMax) {
this.backAndForthLeftMax = left;
}
if (top > this.backAndForthTopMax) {
this.backAndForthTopMax = top;
}
log('dragElement');
}
}
(function() {
'use strict';
const icon = document.createElement('tr-icon');// 翻译图标
icon.id = 'cantonese_translate';
icon.style.cssText = 'display: none;top: 186px;left: 37px;position: absolute;z-index: 2147473647;cursor:move;';
const imgShyyp = getImg(shyypIconUrl, 'shyyp', '长文注音');
const imgShyyp01 = getImg(shyypIconMosaicUrl, 'shyyp01', '单字查询');
const imgShyyp02 = getImg(shyypIconOnlyHeadUrl, 'shyyp03', '粤普转换');
// 绑定图标拖动事件
const iconDrag = new Drag(icon);
//区分拖动和点击事件,有足够位移才触发窗口事件
imgShyyp.addEventListener('mouseup', longscriptPopup);
imgShyyp01.addEventListener('mouseup', singleWorldPopup);
imgShyyp02.addEventListener('mouseup', toMandarionOrCantonese);
icon.appendChild(imgShyyp01);
icon.appendChild(imgShyyp);
icon.appendChild(imgShyyp02);
document.body.appendChild(icon);
// 鼠标事件:防止选中的文本消失;显示、隐藏翻译图标
document.addEventListener('mouseup', showIcon);
// 选中变化事件
document.addEventListener('selectionchange', showIcon);
document.addEventListener('touchend', showIcon);
//粤普转换自动化操作
window.onload = () =>{
checkUrlAndExecute(async function auto() {
selected = await GM.getValue('selectedText', '');
await new Promise(resolve => setTimeout(resolve, 200));
var textareaEle = document.querySelector("#stage0");
textareaEle.value = selected;
} ,shyypConvertUrl)
}
var cssText = `
#cantonese_translate img:hover{
cursor:pointer;
}
#cantonese_translate img:hover{
border:1px solid #1ABB27
}
#cantonese_translate img{
cursor:pointer;
display:inline-block;
width:20px;
height:20px;
border:1px solid #dfe1e5;
border-radius:4px;
background-color:rgba(255,255,255,1);
padding:2px;
margin:0;
margin-right:5px;
box-sizing:content-box;vertical-align:middle}
`
GMaddStyle(cssText);
/* 获取长文注音路径参数x请求的json*/
function createMutationJson(srcValue) {
const queryTemplate = `mutation Submit($src: String!){ submitSrc(src: $src) }`;
const variables = { src: srcValue };
const query = `{"query":"${queryTemplate}","variables":${JSON.stringify(variables)}}`;
return JSON.parse(query);
}
async function sendPostRequest(url, data) {
const body = JSON.stringify(data);
const headers = new Headers({
'Content-Type': 'application/json'
});
const options = {
method: 'POST',
headers,
body
};
try {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
} catch (error) {
console.error('Failed to fetch:', error);
}
}
function sendPostRequestWithGM(url, data) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'POST',
url: url,
data: JSON.stringify(data),
headers: {
'Content-Type': 'application/json'
},
onload: function(response) {
resolve(JSON.parse(response.responseText));
},
onerror: function(error) {
reject(error);
}
});
});
}
/* 长文获取整页html,目前不需要 */
async function sendGetRequestHtml(urlBase, param) {
const url = new URL(urlBase);
url.searchParams.set('x', param);
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const htmlContent = await response.text();
return htmlContent;
} catch (error) {
console.error('Failed to fetch:', error);
}
}
function getImg(src01, alt01, title01, options = {}) {
// 创建一个新的 img 元素
const img = document.createElement('img');
// 设置 img 元素的基本属性
img.src = src01;
img.alt = alt01;
img.title = title01;
// 设置额外的属性
if (options.width) {
img.width = options.width;
}
if (options.height) {
img.height = options.height;
}
if (options.className) {
img.className = options.className;
}
if (options.style) {
Object.keys(options.style).forEach(key => {
img.style[key] = options.style[key];
});
}
// 返回创建的 img 元素
return img;
}
/** 弹出居中窗口 */
function popupCenter(url, title = '_blank', w, h) {
// 检查参数有效性
if (!url || typeof url !== 'string') {
console.error('Invalid URL provided');
return null;
}
// 设置默认标题和窗口尺寸
title = title || '_blank';
w = Math.min(w, screen.availWidth);
h = Math.min(h, screen.availHeight);
// 计算居中位置
let x = (screen.availWidth - w) / 2;
let y = (screen.availHeight - h) / 2;
// 确保窗口不会超出屏幕边界
x = Math.max(x, 0);
y = Math.max(y, 0);
// 打开新窗口
let win;
try {
win = window.open(url, title, `width=${w},height=${h},left=${x},top=${y}`);
if (win) {
win.focus();
let closeNewWindow = window.addEventListener('focus', function() {
win.close();
window.removeEventListener('focus', closeNewWindow);
});
} else {
throw new Error('Failed to open the window');
}
} catch (e) {
console.error('Error opening the window:', e);
}
return win;
}
/**显示 icon*/
function showIcon(e) {
log('showIcon event:', e);
let offsetX = -100; // 横坐标翻译图标偏移
let offsetY = -40; // 纵坐标翻译图标偏移
// 更新翻译图标 X、Y 坐标
if (e.pageX && e.pageY) { // 鼠标
log('mouse pageX/Y');
pageX = e.pageX;
pageY = e.pageY;
}
if (e.changedTouches) { // 触屏
if (e.changedTouches.length > 0) { // 多点触控选取第 1 个
log('touch pageX/Y');
pageX = e.changedTouches[0].pageX;
pageY = e.changedTouches[0].pageY;
// 触屏修改翻译图标偏移(Android、iOS 选中后的动作菜单一般在当前文字顶部,翻译图标则放到底部)
offsetX = -26; // 单个翻译图标块宽度
offsetY = 16 * 3; // 一般字体高度的 3 倍,距离系统自带动作菜单、选择光标太近会导致无法点按
}
}
log(`selected:${selected}, pageX:${pageX}, pageY:${pageY}`)
if (e.target == icon || (e.target.parentNode && e.target.parentNode == icon)) { // 点击了翻译图标
e.preventDefault();
return;
}
selected = window.getSelection().toString().trim(); // 当前选中文本
GM_setValue('selectedText', selected);
log(`selected:${selected}, icon display:${icon.style.display}`);
if (selected && icon.style.display != 'block' && pageX && pageY) { // 显示翻译图标
log('show icon');
icon.style.top = `${pageY + offsetY}px`;
icon.style.left = `${pageX + offsetX}px`;
icon.style.display = 'block';
// 兼容部分 Content Security Policy
icon.style.position = 'absolute';
icon.style.zIndex = zIndex;
} else if (!selected) { // 隐藏翻译图标
log('hide icon');
hideIcon();
}
}
/**隐藏 icon*/
function hideIcon() {
icon.style.display = 'none';
pageX = 0;
pageY = 0;
}
/* 长文注音弹出 */
async function longscriptPopup(){
try {
const response = await sendPostRequestWithGM(shyypTokenUrl, createMutationJson(selected));
textEncry = response.data.submitSrc;
console.log(textEncry);
await new Promise(resolve => setTimeout(resolve, 100));
if (iconDrag.backAndForthLeftMax <= dragFluctuation && iconDrag.backAndForthTopMax <= dragFluctuation) {
popupCenter(`${shyypLongScriptUrl}?x=${textEncry}`, '长文注音', 1024, 800);
}
} catch (error) {
console.error('Error:', error);
}
}
/* 单字弹出 */
async function singleWorldPopup(){
try {
await new Promise(resolve => setTimeout(resolve, 100));
if (iconDrag.backAndForthLeftMax <= dragFluctuation && iconDrag.backAndForthTopMax <= dragFluctuation) {
popupCenter(`${shyypSingleWorldUrl}${selected}`, '单字查询', 1024, 800);
}
} catch (error) {
console.error('Error:', error);
}
}
/* 粤普转换弹出 */
async function toMandarionOrCantonese(){
try {
await new Promise(resolve => setTimeout(resolve, 100));
if (iconDrag.backAndForthLeftMax <= dragFluctuation && iconDrag.backAndForthTopMax <= dragFluctuation) {
popupCenter(shyypConvertUrl, '粤普转换', 1024, 800);
}
} catch (error) {
console.error('Error:', error);
}
}
/* 新窗口自动化操作 */
function checkUrlAndExecute(customFunction, targetUrl) {
// 获取当前页面的完整URL
const currentUrl = window.location.href;
// 检查当前URL是否与目标URL相等
if (currentUrl === targetUrl) {
// 如果URL匹配,则执行自定义函数
customFunction();
}
}
})();
/**日志输出*/
function log(...args) {
const debug = false;
if (!debug) {
return;
}
if (args) {
for (let i = 0; i < args.length; i++) {
console.log(args[i]);
}
}
}
function GMaddStyle(css){
var myStyle = document.createElement('style');
myStyle.textContent = css;
var doc = document.head || document.documentElement;
doc.appendChild(myStyle);
}