coolenglish-英文-外掛-浮動翻譯按鈕

自動選取文字即翻譯,四色按鈕,語音朗讀,穩定多來源翻譯 API(Google + LibreTranslate)

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         coolenglish-英文-外掛-浮動翻譯按鈕
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  自動選取文字即翻譯,四色按鈕,語音朗讀,穩定多來源翻譯 API(Google + LibreTranslate)
// @author       issac
// @match        *://*/*
// @grant        GM_addStyle
// @license      GPL-3.0 License
// @run-at       document-end
// ==/UserScript==
 
(function() {
  'use strict';
  const TARGET_LANG = 'zh-TW';
 
  GM_addStyle(`
    #miniTrans {
      position: fixed; right: 20px; bottom: 20px;
      width: 280px; background: #0d1117; color: #fff;
      font-family: sans-serif; border: 1px solid #333;
      border-radius: 8px; box-shadow: 0 5px 20px rgba(0,0,0,0.5);
      z-index: 99999; resize: both; overflow: hidden;
    }
    #miniTrans header {
      background: #161b22; padding: 5px 8px;
      font-size: 13px; cursor: move; display: flex;
      justify-content: space-between; align-items: center;
    }
    #miniTrans header button {
      background: transparent; border: none; color: #ccc;
      cursor: pointer; font-size: 14px;
    }
    #miniTrans textarea {
      width: 95%; margin: 5px; height: 60px;
      border: none; border-radius: 5px; padding: 5px;
      background: #161b22; color: #fff; font-size: 13px; resize: vertical;
    }
    #miniTrans .btns {
      display: flex; justify-content: space-around; padding: 5px; gap: 4px;
    }
    #miniTrans button {
      flex: 1; border: none; padding: 4px 6px;
      border-radius: 5px; cursor: pointer; font-size: 12px;
      color: #fff; transition: background 0.2s;
    }
    #mt-trans { background: #2563eb; }
    #mt-trans:hover { background: #1d4ed8; }
    #mt-speak { background: #22c55e; }
    #mt-speak:hover { background: #16a34a; }
    #mt-src { background: #eab308; color:#222; }
    #mt-src:hover { background: #ca8a04; color:#111; }
    #mt-clear { background: #ef4444; }
    #mt-clear:hover { background: #dc2626; }
    #miniTrans.min { height: 28px; overflow: hidden; }
  `);
 
  const box = document.createElement('div');
  box.id = 'miniTrans';
  box.innerHTML = `
    <header><span>翻譯小窗</span><button id="mt-min">−</button></header>
    <textarea id="mt-input" placeholder="選取文字後自動翻譯"></textarea>
    <textarea id="mt-output" placeholder="翻譯結果" readonly></textarea>
    <div class="btns">
      <button id="mt-trans">翻譯</button>
      <button id="mt-speak">唸譯文</button>
      <button id="mt-src">唸原文</button>
      <button id="mt-clear">清除</button>
    </div>
  `;
  document.body.appendChild(box);
 
  const header = box.querySelector('header');
  const minBtn = box.querySelector('#mt-min');
  const input = box.querySelector('#mt-input');
  const output = box.querySelector('#mt-output');
  const btnTrans = box.querySelector('#mt-trans');
  const btnSpeak = box.querySelector('#mt-speak');
  const btnSrc = box.querySelector('#mt-src');
  const btnClear = box.querySelector('#mt-clear');
 
  // 拖曳功能
  (function dragElement(el, handle) {
    let x=0, y=0, dx=0, dy=0, dragging=false;
    handle.addEventListener('mousedown', e => {
      dragging=true; x=e.clientX; y=e.clientY;
      e.preventDefault();
    });
    window.addEventListener('mousemove', e => {
      if(!dragging) return;
      dx=e.clientX-x; dy=e.clientY-y;
      const rect = el.getBoundingClientRect();
      el.style.left = rect.left+dx+'px';
      el.style.top = rect.top+dy+'px';
      el.style.right = 'auto';
      el.style.bottom = 'auto';
      x=e.clientX; y=e.clientY;
    });
    window.addEventListener('mouseup', ()=>dragging=false);
  })(box, header);
 
  // 最小化
  let minimized = false;
  minBtn.onclick = () => {
    minimized = !minimized;
    box.classList.toggle('min');
    minBtn.textContent = minimized ? '+' : '−';
  };
 
  // 清除
  btnClear.onclick = () => {
    input.value = '';
    output.value = '';
  };
 
  // 翻譯函式(多來源備援)
  async function translateText(text) {
    const sources = [
      async () => {
        const res = await fetch('https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=' + TARGET_LANG + '&dt=t&q=' + encodeURIComponent(text));
        const data = await res.json();
        return data[0].map(x => x[0]).join('');
      },
      async () => {
        const res = await fetch('https://libretranslate.de/translate', {
          method: 'POST',
          headers: {'Content-Type':'application/json'},
          body: JSON.stringify({q:text, source:'auto', target:TARGET_LANG})
        });
        const data = await res.json();
        return data.translatedText;
      }
    ];
    for (const fn of sources) {
      try {
        const result = await fn();
        if (result) return result;
      } catch (e) { /* 繼續嘗試下一個 */ }
    }
    throw new Error('所有翻譯來源皆失敗');
  }
 
  // 手動翻譯按鈕
  btnTrans.onclick = async () => {
    const text = input.value.trim();
    if(!text) return;
    output.value = '翻譯中...';
    try {
      const translated = await translateText(text);
      output.value = translated;
    } catch (e) {
      output.value = '翻譯錯誤:無法取得結果';
    }
  };
 
  // 語音
  function speak(text, lang) {
    if(!text) return;
    speechSynthesis.cancel();
    const u = new SpeechSynthesisUtterance(text);
    u.lang = lang || 'zh-TW';
    speechSynthesis.speak(u);
  }
  btnSpeak.onclick = () => speak(output.value, 'zh-TW');
  btnSrc.onclick = () => speak(input.value, 'auto');
 
  // 自動偵測選取文字 → 自動翻譯
  let lastSel = '';
  document.addEventListener('mouseup', () => {
    setTimeout(async () => {
      const sel = window.getSelection().toString().trim();
      if(sel && sel !== lastSel) {
        lastSel = sel;
        input.value = sel;
        output.value = '翻譯中...';
        try {
          const translated = await translateText(sel);
          output.value = translated;
        } catch (e) {
          output.value = '翻譯錯誤:無法取得結果';
        }
      }
    }, 100);
  });
})();