粤语划词翻译

快捷粤语翻译查询

目前為 2024-10-18 提交的版本,檢視 最新版本

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