Compare commits

..

1 Commits

Author SHA1 Message Date
scout
0bb1ddbf88 翻译 2024-11-13 11:55:14 +08:00
16 changed files with 1099 additions and 771 deletions

120
README.md
View File

@ -1,89 +1,85 @@
# 丰链工具箱 Chrome 扩展
一个集成多种实用工具的 Chrome 扩展包括二维码生成、Word 转 HTML、Word 文档翻译等功能。
一个实用的 Chrome 扩展工具集合集成了二维码生成、Word 转 HTML、中英文翻译等实用功能。
## 功能特
## 功能特
### 1. 二维码生成
### 二维码生成器 🔲
- 自动获取当前标签页 URL 并生成二维码
- 支持手动输入文本或链接生成二维码
- 高清晰度二维码输出
- 支持手动输入文本/链接生成二维码
- 生成高清晰度二维码,支持长文本
- 即时预览生成结果
### 2. Word 转 HTML
### Word 转 HTML 转换器 📄
- 支持 .docx 文件转换为 HTML
- 保原文档格式和样式
- 保原文档格式和样式
- 实时预览转换结果
- 一键下载 HTML 文件
- 支持一键下载转换后的 HTML 文件
- 自动处理图片和表格
### 3. Word 文档翻译
- 支持中文文档翻译为英文、日文、繁体中文
- 完整保留原文档格式(包括字体、颜色、加粗等样式)
- 支持表格内容翻译
- 实时显示翻译进度
- 保持文档的换行格式
- 自动重试机制,提高翻译稳定性
### 中英文翻译 🔤
- 支持 Windows(Ctrl+Q) 和 Mac(Command+Q) 快捷键激活
- 框选任意区域中文文本进行翻译
- 支持中英互译和繁体转换
- 翻译结果即时显示在选区旁边
- 点击任意位置关闭翻译结果
## 安装说明
## 系统要求
### 扩展安装
- Chrome 88+ 或其他基于 Chromium 的浏览器
- Windows/MacOS/Linux 系统均可运行
## 安装方法
### 从 Chrome 商店安装
1. 访问 Chrome 网上应用店
2. 搜索"丰链工具箱"
3. 点击"添加至 Chrome"
### 开发者模式安装
1. 下载本项目代码
2. 打开 Chrome 浏览器,进入扩展管理页面 (chrome://extensions/)
3. 开启"开发者模式"
4. 点击"加载已解压的扩展程序",选择项目目录
2. 打开 Chrome 浏览器,进入扩展程序页面 (chrome://extensions/)
3. 开启右上角的"开发者模式"
4. 点击"加载已解压的扩展程序"
5. 选择项目文件夹即可完成安装
### 后端服务安装
1. 确保已安装 Python 3.8+
2. 进入 backend 目录
3. 创建虚拟环境(可选):
```bash
python -m venv venv
# Windows
venv\Scripts\activate
# Mac/Linux
source venv/bin/activate
```
4. 安装依赖:
```bash
pip install -r requirements.txt
```
5. 启动后端服务:
```bash
python app.py
```
## 使用说明
## 使用方法
### 二维码生成
1. 点击扩展图标打开工具箱
2. 默认显示当前页面 URL 的二维码
3. 可在输入框中修改文本,点击"生成二维码"更新
2. 默认显示当前页面的二维码
3. 可在输入框中输入任意文本/链接
4. 点击"生成二维码"按钮即可
### Word 转 HTML
1. 选择"Word转HTML"选项卡
2. 点击上传区域选择 .docx 文件
3. 等待转换完成后预览结
1. 点击"Word转HTML"标签页
2. 点击选择或拖拽上传 Word 文档
3. 等待转换完成后可预览效果
4. 点击"下载HTML"保存文件
### Word 文档翻译
1. 选择"Word翻译"选项卡
2. 点击上传区域选择要翻译的 Word 文档
3. 从下拉菜单选择目标语言(英文/日文/繁体中文)
4. 点击"开始翻译"按钮
5. 等待翻译完成,自动下载翻译后的文档
## 注意事项
1. 使用翻译功能时需要确保后端服务正在运行
2. 翻译功能使用百度翻译 API请确保网络连接正常
3. 大文件翻译可能需要较长时间,请耐心等待
4. 建议定期清理 temp 目录下的临时文件
### 翻译功能
1. 点击"翻译设置"标签页开启功能
2. 选择目标语言(英文/繁体中文)
3. 在任意页面按下快捷键(Ctrl+Q/Command+Q)
4. 框选需要翻译的文本
5. 翻译结果会显示在选区旁边
## 技术栈
- 前端HTML, CSS, JavaScript
- 后端Python, Flask
- 依赖库python-docx, mammoth.js, qrcode.js
- HTML5/CSS3
- JavaScript ES6+
- Chrome Extension API
- QRCode.js - 二维码生成
- Mammoth.js - Word 文档转换
- 百度翻译 API - 文本翻译
## 开发者
Made by Scout
## 更新日志
### v1.0.0
- 初始版本发布
- 实现基础功能二维码生成、Word转HTML、翻译
- 支持 Windows/Mac 快捷键

View File

@ -1,85 +0,0 @@
from flask import Flask, request, send_file
from flask_cors import CORS
import os
from utils.docx_translator import DocxTranslator
import shutil
import time
app = Flask(__name__)
CORS(app, resources={
r"/*": {
"origins": ["chrome-extension://*"],
"methods": ["POST", "OPTIONS", "GET"],
"allow_headers": ["Content-Type"]
}
})
# 全局变量存储翻译进度
translation_progress = 0
UPLOAD_FOLDER = 'temp'
if not os.path.exists(UPLOAD_FOLDER):
os.makedirs(UPLOAD_FOLDER)
def update_progress(progress):
global translation_progress
translation_progress = progress
@app.route('/progress', methods=['GET'])
def get_progress():
return {'progress': translation_progress}
@app.route('/translate', methods=['POST'])
def translate_docx():
global translation_progress
translation_progress = 0
input_path = None
output_path = None
if 'file' not in request.files:
return {'error': 'No file provided'}, 400
file = request.files['file']
target_lang = request.form.get('target_lang', 'en')
if file.filename == '':
return {'error': 'No file selected'}, 400
try:
timestamp = str(int(time.time()))
safe_filename = f"{timestamp}_{file.filename}"
input_path = os.path.join(UPLOAD_FOLDER, safe_filename)
file.save(input_path)
translator = DocxTranslator()
output_path = translator.translate_document(
input_path,
target_lang=target_lang,
progress_callback=update_progress
)
temp_output = output_path + '.tmp'
shutil.copy2(output_path, temp_output)
return send_file(
temp_output,
as_attachment=True,
download_name=f"translated_{file.filename}"
)
except Exception as e:
return {'error': str(e)}, 500
finally:
try:
if input_path and os.path.exists(input_path):
os.remove(input_path)
if output_path and os.path.exists(output_path):
os.remove(output_path)
if 'temp_output' in locals() and os.path.exists(temp_output):
os.remove(temp_output)
except Exception as e:
print(f"Error cleaning up files: {e}")
if __name__ == '__main__':
app.run(debug=True, port=5000)

View File

@ -1,5 +0,0 @@
flask==2.3.3
werkzeug==2.3.7
flask-cors==4.0.0
python-docx==0.8.11
requests==2.31.0

View File

@ -1,244 +0,0 @@
from docx import Document
import hashlib
import time
import requests
import os
class DocxTranslator:
def __init__(self):
self.appid = "20241112002200806"
self.secret_key = "preM0becByYCdotRTP_a"
self.api_url = "https://api.fanyi.baidu.com/api/trans/vip/translate"
self.max_length = 2000
self.lang_map = {
'en': 'en', # English
'jp': 'jp', # Japanese
'cht': 'cht' # Traditional Chinese
}
def _get_sign(self, text, salt):
sign_str = f"{self.appid}{text}{salt}{self.secret_key}"
return hashlib.md5(sign_str.encode()).hexdigest()
def translate_text(self, text, to_lang):
target_lang = self.lang_map.get(to_lang, to_lang)
if len(text) > self.max_length:
segments = text.split('')
current_segment = ''
translated_segments = []
for segment in segments:
if len(current_segment) + len(segment) < self.max_length:
current_segment += segment + ''
else:
if current_segment:
translated_segments.append(self._translate_segment(current_segment, target_lang))
current_segment = segment + ''
if current_segment:
translated_segments.append(self._translate_segment(current_segment, target_lang))
return ''.join(translated_segments)
else:
return self._translate_segment(text, target_lang)
def _translate_segment(self, text, to_lang):
if not text.strip():
return text
salt = str(int(time.time()))
sign = self._get_sign(text, salt)
params = {
'q': text,
'from': 'zh',
'to': to_lang,
'appid': self.appid,
'salt': salt,
'sign': sign
}
max_retries = 3
for attempt in range(max_retries):
try:
response = requests.get(self.api_url, params=params)
result = response.json()
if 'error_code' in result:
if attempt < max_retries - 1:
time.sleep(1)
continue
raise Exception(f"Translation error: {result['error_msg']}")
return result['trans_result'][0]['dst']
except Exception as e:
if attempt < max_retries - 1:
time.sleep(1)
continue
raise e
def translate_document(self, input_path, target_lang='en', progress_callback=None):
doc = Document(input_path)
output_path = input_path.replace('.docx', f'_translated.docx')
try:
# 计算总翻译项
total_items = len(doc.paragraphs)
for table in doc.tables:
total_items += sum(len(row.cells) for row in table.rows)
current_item = 0
# 翻译段落
for paragraph in doc.paragraphs:
if paragraph.text.strip():
try:
# 保存原始的段落格式和换行
runs = paragraph.runs
original_runs = []
# 收集每个run的文本和格式信息保留原始换行符
for run in runs:
text = run.text
if text: # 不去除空白字符,保留原始格式
original_runs.append({
'text': text,
'bold': run.bold,
'italic': run.italic,
'underline': run.underline,
'font_name': run.font.name,
'font_size': run.font.size,
'color_rgb': run.font.color.rgb if run.font.color else None,
'break_type': 'break' if any(text.endswith(x) for x in ['\n', '\v', '\r']) else None
})
# 清除原有内容但保持段落格式
paragraph.clear()
# 分别翻译和添加每个run的文本
for i, orig_run in enumerate(original_runs):
if orig_run['text']:
# 保留换行符
has_break = orig_run['break_type'] == 'break'
text_to_translate = orig_run['text'].rstrip('\n\r\v')
# 翻译非空文本
if text_to_translate.strip():
translated_text = self.translate_text(text_to_translate, target_lang)
else:
translated_text = text_to_translate
# 创建新的run并应用格式
new_run = paragraph.add_run()
new_run.bold = orig_run['bold']
new_run.italic = orig_run['italic']
new_run.underline = orig_run['underline']
new_run.font.name = orig_run['font_name']
if orig_run['font_size']:
new_run.font.size = orig_run['font_size']
if orig_run['color_rgb']:
new_run.font.color.rgb = orig_run['color_rgb']
# 添加翻译后的文本
new_run.text = translated_text
# 如果原文有换行,添加换行符
if has_break:
new_run.add_break() # 不指定类型,使用默认换行
except Exception as e:
print(f"Error translating paragraph: {e}")
# 保留原文和格式
paragraph.clear()
for run in runs:
new_run = paragraph.add_run(run.text)
new_run.bold = run.bold
new_run.italic = run.italic
new_run.underline = run.underline
new_run.font.name = run.font.name
if run.font.size:
new_run.font.size = run.font.size
if run.font.color and run.font.color.rgb:
new_run.font.color.rgb = run.font.color.rgb
current_item += 1
if progress_callback:
progress_callback(int((current_item / total_items) * 100))
# 翻译表格 (使用相同的逻辑处理表格单元格)
for table in doc.tables:
for row in table.rows:
for cell in row.cells:
if cell.text.strip():
try:
for paragraph in cell.paragraphs:
runs = paragraph.runs
original_runs = []
for run in runs:
text = run.text
if text:
original_runs.append({
'text': text,
'bold': run.bold,
'italic': run.italic,
'underline': run.underline,
'font_name': run.font.name,
'font_size': run.font.size,
'color_rgb': run.font.color.rgb if run.font.color else None,
'break_type': 'break' if any(text.endswith(x) for x in ['\n', '\v', '\r']) else None
})
paragraph.clear()
for i, orig_run in enumerate(original_runs):
if orig_run['text']:
has_break = orig_run['break_type'] == 'break'
text_to_translate = orig_run['text'].rstrip('\n\r\v')
if text_to_translate.strip():
translated_text = self.translate_text(text_to_translate, target_lang)
else:
translated_text = text_to_translate
new_run = paragraph.add_run()
new_run.bold = orig_run['bold']
new_run.italic = orig_run['italic']
new_run.underline = orig_run['underline']
new_run.font.name = orig_run['font_name']
if orig_run['font_size']:
new_run.font.size = orig_run['font_size']
if orig_run['color_rgb']:
new_run.font.color.rgb = orig_run['color_rgb']
new_run.text = translated_text
if has_break:
new_run.add_break()
except Exception as e:
print(f"Error translating cell: {e}")
# 保留原文和格式
for paragraph in cell.paragraphs:
paragraph.clear()
for run in runs:
new_run = paragraph.add_run(run.text)
new_run.bold = run.bold
new_run.italic = run.italic
new_run.underline = run.underline
new_run.font.name = run.font.name
if run.font.size:
new_run.font.size = run.font.size
if run.font.color and run.font.color.rgb:
new_run.font.color.rgb = run.font.color.rgb
current_item += 1
if progress_callback:
progress_callback(int((current_item / total_items) * 100))
doc.save(output_path)
return output_path
finally:
del doc

342
background.js Normal file
View File

@ -0,0 +1,342 @@
// 存储注入状态
let injectedTabs = new Set();
// 监听快捷键命令
chrome.commands.onCommand.addListener(async (command) => {
if (command === 'trigger-select') {
try {
const result = await chrome.storage.sync.get(['translateEnabled']);
console.log('Translation enabled status:', result.translateEnabled);
// 只有当明确设置为 false 时才禁用
if (result.translateEnabled === false) {
console.log('Translation is disabled');
return;
}
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (tab) {
await injectScriptsIfNeeded(tab.id);
}
} catch (error) {
console.error('Error checking translation status:', error);
}
}
});
// 处理翻译请求
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === 'translate') {
handleTranslation(request.text)
.then(sendResponse)
.catch(error => {
console.error('Translation error:', error);
sendResponse({ error: 'Translation failed' });
});
return true;
}
});
// 注入脚本的函数
async function injectScriptsIfNeeded(tabId) {
try {
// 如果已经注入过,直接发送消息
if (injectedTabs.has(tabId)) {
await chrome.tabs.sendMessage(tabId, { action: 'startSelection' });
return;
}
// 注入 CSS
await chrome.scripting.insertCSS({
target: { tabId },
files: ['content.css']
});
// 先注入 MD5
await chrome.scripting.executeScript({
target: { tabId },
files: ['md5.js']
});
// 再注入 content script
await chrome.scripting.executeScript({
target: { tabId },
files: ['content.js']
});
// 记录已注入状态
injectedTabs.add(tabId);
// 等待一小段时间确保脚本加载完成
await new Promise(resolve => setTimeout(resolve, 100));
// 发送消息
await chrome.tabs.sendMessage(tabId, { action: 'startSelection' });
} catch (error) {
console.error('Injection error:', error);
}
}
// 监听标签页更新,清除注入状态
chrome.tabs.onUpdated.addListener((tabId, changeInfo) => {
if (changeInfo.status === 'loading') {
injectedTabs.delete(tabId);
}
});
// 监听标签页移除,清除注入状态
chrome.tabs.onRemoved.addListener((tabId) => {
injectedTabs.delete(tabId);
});
// 添加 MD5 函数
function md5(string) {
function md5_RotateLeft(lValue, iShiftBits) {
return (lValue << iShiftBits) | (lValue >>> (32 - iShiftBits));
}
function md5_AddUnsigned(lX, lY) {
var lX4, lY4, lX8, lY8, lResult;
lX8 = (lX & 0x80000000);
lY8 = (lY & 0x80000000);
lX4 = (lX & 0x40000000);
lY4 = (lY & 0x40000000);
lResult = (lX & 0x3FFFFFFF) + (lY & 0x3FFFFFFF);
if (lX4 & lY4) {
return (lResult ^ 0x80000000 ^ lX8 ^ lY8);
}
if (lX4 | lY4) {
if (lResult & 0x40000000) {
return (lResult ^ 0xC0000000 ^ lX8 ^ lY8);
} else {
return (lResult ^ 0x40000000 ^ lX8 ^ lY8);
}
} else {
return (lResult ^ lX8 ^ lY8);
}
}
function md5_F(x, y, z) { return (x & y) | ((~x) & z); }
function md5_G(x, y, z) { return (x & z) | (y & (~z)); }
function md5_H(x, y, z) { return (x ^ y ^ z); }
function md5_I(x, y, z) { return (y ^ (x | (~z))); }
function md5_FF(a, b, c, d, x, s, ac) {
a = md5_AddUnsigned(a, md5_AddUnsigned(md5_AddUnsigned(md5_F(b, c, d), x), ac));
return md5_AddUnsigned(md5_RotateLeft(a, s), b);
}
function md5_GG(a, b, c, d, x, s, ac) {
a = md5_AddUnsigned(a, md5_AddUnsigned(md5_AddUnsigned(md5_G(b, c, d), x), ac));
return md5_AddUnsigned(md5_RotateLeft(a, s), b);
}
function md5_HH(a, b, c, d, x, s, ac) {
a = md5_AddUnsigned(a, md5_AddUnsigned(md5_AddUnsigned(md5_H(b, c, d), x), ac));
return md5_AddUnsigned(md5_RotateLeft(a, s), b);
}
function md5_II(a, b, c, d, x, s, ac) {
a = md5_AddUnsigned(a, md5_AddUnsigned(md5_AddUnsigned(md5_I(b, c, d), x), ac));
return md5_AddUnsigned(md5_RotateLeft(a, s), b);
}
function md5_ConvertToWordArray(string) {
var lWordCount;
var lMessageLength = string.length;
var lNumberOfWords_temp1 = lMessageLength + 8;
var lNumberOfWords_temp2 = (lNumberOfWords_temp1 - (lNumberOfWords_temp1 % 64)) / 64;
var lNumberOfWords = (lNumberOfWords_temp2 + 1) * 16;
var lWordArray = Array(lNumberOfWords - 1);
var lBytePosition = 0;
var lByteCount = 0;
while (lByteCount < lMessageLength) {
lWordCount = (lByteCount - (lByteCount % 4)) / 4;
lBytePosition = (lByteCount % 4) * 8;
lWordArray[lWordCount] = (lWordArray[lWordCount] | (string.charCodeAt(lByteCount) << lBytePosition));
lByteCount++;
}
lWordCount = (lByteCount - (lByteCount % 4)) / 4;
lBytePosition = (lByteCount % 4) * 8;
lWordArray[lWordCount] = lWordArray[lWordCount] | (0x80 << lBytePosition);
lWordArray[lNumberOfWords - 2] = lMessageLength << 3;
lWordArray[lNumberOfWords - 1] = lMessageLength >>> 29;
return lWordArray;
}
function md5_WordToHex(lValue) {
var WordToHexValue = "", WordToHexValue_temp = "", lByte, lCount;
for (lCount = 0; lCount <= 3; lCount++) {
lByte = (lValue >>> (lCount * 8)) & 255;
WordToHexValue_temp = "0" + lByte.toString(16);
WordToHexValue = WordToHexValue + WordToHexValue_temp.substr(WordToHexValue_temp.length - 2, 2);
}
return WordToHexValue;
}
function md5_Utf8Encode(string) {
string = string.replace(/\r\n/g, "\n");
var utftext = "";
for (var n = 0; n < string.length; n++) {
var c = string.charCodeAt(n);
if (c < 128) {
utftext += String.fromCharCode(c);
} else if ((c > 127) && (c < 2048)) {
utftext += String.fromCharCode((c >> 6) | 192);
utftext += String.fromCharCode((c & 63) | 128);
} else {
utftext += String.fromCharCode((c >> 12) | 224);
utftext += String.fromCharCode(((c >> 6) & 63) | 128);
utftext += String.fromCharCode((c & 63) | 128);
}
}
return utftext;
}
var x = Array();
var k, AA, BB, CC, DD, a, b, c, d;
var S11 = 7, S12 = 12, S13 = 17, S14 = 22;
var S21 = 5, S22 = 9, S23 = 14, S24 = 20;
var S31 = 4, S32 = 11, S33 = 16, S34 = 23;
var S41 = 6, S42 = 10, S43 = 15, S44 = 21;
string = md5_Utf8Encode(string);
x = md5_ConvertToWordArray(string);
a = 0x67452301; b = 0xEFCDAB89; c = 0x98BADCFE; d = 0x10325476;
for (k = 0; k < x.length; k += 16) {
AA = a; BB = b; CC = c; DD = d;
a = md5_FF(a, b, c, d, x[k + 0], S11, 0xD76AA478);
d = md5_FF(d, a, b, c, x[k + 1], S12, 0xE8C7B756);
c = md5_FF(c, d, a, b, x[k + 2], S13, 0x242070DB);
b = md5_FF(b, c, d, a, x[k + 3], S14, 0xC1BDCEEE);
a = md5_FF(a, b, c, d, x[k + 4], S11, 0xF57C0FAF);
d = md5_FF(d, a, b, c, x[k + 5], S12, 0x4787C62A);
c = md5_FF(c, d, a, b, x[k + 6], S13, 0xA8304613);
b = md5_FF(b, c, d, a, x[k + 7], S14, 0xFD469501);
a = md5_FF(a, b, c, d, x[k + 8], S11, 0x698098D8);
d = md5_FF(d, a, b, c, x[k + 9], S12, 0x8B44F7AF);
c = md5_FF(c, d, a, b, x[k + 10], S13, 0xFFFF5BB1);
b = md5_FF(b, c, d, a, x[k + 11], S14, 0x895CD7BE);
a = md5_FF(a, b, c, d, x[k + 12], S11, 0x6B901122);
d = md5_FF(d, a, b, c, x[k + 13], S12, 0xFD987193);
c = md5_FF(c, d, a, b, x[k + 14], S13, 0xA679438E);
b = md5_FF(b, c, d, a, x[k + 15], S14, 0x49B40821);
a = md5_GG(a, b, c, d, x[k + 1], S21, 0xF61E2562);
d = md5_GG(d, a, b, c, x[k + 6], S22, 0xC040B340);
c = md5_GG(c, d, a, b, x[k + 11], S23, 0x265E5A51);
b = md5_GG(b, c, d, a, x[k + 0], S24, 0xE9B6C7AA);
a = md5_GG(a, b, c, d, x[k + 5], S21, 0xD62F105D);
d = md5_GG(d, a, b, c, x[k + 10], S22, 0x2441453);
c = md5_GG(c, d, a, b, x[k + 15], S23, 0xD8A1E681);
b = md5_GG(b, c, d, a, x[k + 4], S24, 0xE7D3FBC8);
a = md5_GG(a, b, c, d, x[k + 9], S21, 0x21E1CDE6);
d = md5_GG(d, a, b, c, x[k + 14], S22, 0xC33707D6);
c = md5_GG(c, d, a, b, x[k + 3], S23, 0xF4D50D87);
b = md5_GG(b, c, d, a, x[k + 8], S24, 0x455A14ED);
a = md5_GG(a, b, c, d, x[k + 13], S21, 0xA9E3E905);
d = md5_GG(d, a, b, c, x[k + 2], S22, 0xFCEFA3F8);
c = md5_GG(c, d, a, b, x[k + 7], S23, 0x676F02D9);
b = md5_GG(b, c, d, a, x[k + 12], S24, 0x8D2A4C8A);
a = md5_HH(a, b, c, d, x[k + 5], S31, 0xFFFA3942);
d = md5_HH(d, a, b, c, x[k + 8], S32, 0x8771F681);
c = md5_HH(c, d, a, b, x[k + 11], S33, 0x6D9D6122);
b = md5_HH(b, c, d, a, x[k + 14], S34, 0xFDE5380C);
a = md5_HH(a, b, c, d, x[k + 1], S31, 0xA4BEEA44);
d = md5_HH(d, a, b, c, x[k + 4], S32, 0x4BDECFA9);
c = md5_HH(c, d, a, b, x[k + 7], S33, 0xF6BB4B60);
b = md5_HH(b, c, d, a, x[k + 10], S34, 0xBEBFBC70);
a = md5_HH(a, b, c, d, x[k + 13], S31, 0x289B7EC6);
d = md5_HH(d, a, b, c, x[k + 0], S32, 0xEAA127FA);
c = md5_HH(c, d, a, b, x[k + 3], S33, 0xD4EF3085);
b = md5_HH(b, c, d, a, x[k + 6], S34, 0x4881D05);
a = md5_HH(a, b, c, d, x[k + 9], S31, 0xD9D4D039);
d = md5_HH(d, a, b, c, x[k + 12], S32, 0xE6DB99E5);
c = md5_HH(c, d, a, b, x[k + 15], S33, 0x1FA27CF8);
b = md5_HH(b, c, d, a, x[k + 2], S34, 0xC4AC5665);
a = md5_II(a, b, c, d, x[k + 0], S41, 0xF4292244);
d = md5_II(d, a, b, c, x[k + 7], S42, 0x432AFF97);
c = md5_II(c, d, a, b, x[k + 14], S43, 0xAB9423A7);
b = md5_II(b, c, d, a, x[k + 5], S44, 0xFC93A039);
a = md5_II(a, b, c, d, x[k + 12], S41, 0x655B59C3);
d = md5_II(d, a, b, c, x[k + 3], S42, 0x8F0CCC92);
c = md5_II(c, d, a, b, x[k + 10], S43, 0xFFEFF47D);
b = md5_II(b, c, d, a, x[k + 1], S44, 0x85845DD1);
a = md5_II(a, b, c, d, x[k + 8], S41, 0x6FA87E4F);
d = md5_II(d, a, b, c, x[k + 15], S42, 0xFE2CE6E0);
c = md5_II(c, d, a, b, x[k + 6], S43, 0xA3014314);
b = md5_II(b, c, d, a, x[k + 13], S44, 0x4E0811A1);
a = md5_II(a, b, c, d, x[k + 4], S41, 0xF7537E82);
d = md5_II(d, a, b, c, x[k + 11], S42, 0xBD3AF235);
c = md5_II(c, d, a, b, x[k + 2], S43, 0x2AD7D2BB);
b = md5_II(b, c, d, a, x[k + 9], S44, 0xEB86D391);
a = md5_AddUnsigned(a, AA);
b = md5_AddUnsigned(b, BB);
c = md5_AddUnsigned(c, CC);
d = md5_AddUnsigned(d, DD);
}
return (md5_WordToHex(a) + md5_WordToHex(b) + md5_WordToHex(c) + md5_WordToHex(d)).toLowerCase();
}
async function handleTranslation(text) {
try {
// 获取目标语言设置
const { targetLang } = await chrome.storage.sync.get(['targetLang']);
const to = targetLang || 'en';
const appid = '20241112002200806';
const key = 'preM0becByYCdotRTP_a';
const salt = Date.now().toString();
const from = 'zh';
const str1 = appid + text + salt + key;
const sign = md5(str1);
const params = new URLSearchParams({
q: text,
from: from,
to: to,
appid: appid,
salt: salt,
sign: sign
});
const url = 'https://api.fanyi.baidu.com/api/trans/vip/translate';
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: params.toString()
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
console.log('Translation result:', result);
if (result.error_code) {
throw new Error(result.error_msg || 'Translation failed');
}
return result;
} catch (error) {
console.error('Translation error:', error);
return {
error: true,
message: error.message || 'Translation failed',
trans_result: [{
dst: 'Translation failed: ' + (error.message || 'Unknown error')
}]
};
}
}
// 监听扩展安装或更新
chrome.runtime.onInstalled.addListener(() => {
console.log('Extension installed/updated');
});

30
content.css Normal file
View File

@ -0,0 +1,30 @@
.selection-overlay {
position: fixed;
border: 2px dashed #4285f4;
background: rgba(66, 133, 244, 0.1);
z-index: 10000;
pointer-events: none;
}
.translation-popup {
position: fixed;
background: white;
padding: 10px;
border-radius: 4px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
z-index: 10001;
max-width: 300px;
font-size: 14px;
line-height: 1.4;
}
.page-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.1);
z-index: 9999;
cursor: crosshair;
}

359
content.js Normal file
View File

@ -0,0 +1,359 @@
// 检查是否已经初始化
if (window.translationToolInitialized) {
throw new Error('Translation tool already initialized');
}
window.translationToolInitialized = true;
let isSelecting = false;
let startX, startY;
let selectionElement = null;
let translationPopup = null;
// 监听来自background的消息
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === 'startSelection') {
startSelectionMode();
}
});
function startSelectionMode() {
if (isSelecting) return;
isSelecting = true;
const overlay = document.createElement('div');
overlay.className = 'page-overlay';
document.body.appendChild(overlay);
selectionElement = document.createElement('div');
selectionElement.className = 'selection-overlay';
document.body.appendChild(selectionElement);
document.addEventListener('mousedown', handleMouseDown);
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
document.addEventListener('keydown', handleKeyDown);
}
function handleMouseDown(e) {
if (!isSelecting) return;
startX = e.clientX;
startY = e.clientY;
updateSelectionBox(e);
}
function handleMouseMove(e) {
if (!isSelecting || !startX) return;
updateSelectionBox(e);
}
function handleMouseUp(e) {
if (!isSelecting) return;
const selectedText = getTextFromArea(
Math.min(startX, e.clientX),
Math.min(startY, e.clientY),
Math.abs(e.clientX - startX),
Math.abs(e.clientY - startY)
);
if (selectedText.trim()) {
translateText(selectedText.trim(), e.clientX, e.clientY);
}
cleanup();
}
function updateSelectionBox(e) {
const left = Math.min(startX, e.clientX);
const top = Math.min(startY, e.clientY);
const width = Math.abs(e.clientX - startX);
const height = Math.abs(e.clientY - startY);
selectionElement.style.left = left + 'px';
selectionElement.style.top = top + 'px';
selectionElement.style.width = width + 'px';
selectionElement.style.height = height + 'px';
}
function getTextFromArea(left, top, width, height) {
const elements = document.elementsFromPoint(left + width/2, top + height/2);
for (const element of elements) {
if (element.textContent && element !== selectionElement) {
return element.textContent;
}
}
return '';
}
async function translateText(text, x, y) {
try {
// 通过 chrome.runtime.sendMessage 发送翻译请求
const result = await chrome.runtime.sendMessage({
action: 'translate',
text: text
});
if (result && result.trans_result && result.trans_result[0]) {
showTranslation(result.trans_result[0].dst, x, y);
} else {
showTranslation('Translation failed', x, y);
}
} catch (error) {
console.error('Translation failed:', error);
showTranslation('Translation failed', x, y);
}
}
function showTranslation(translatedText, x, y) {
if (translationPopup) {
translationPopup.remove();
}
translationPopup = document.createElement('div');
translationPopup.className = 'translation-popup';
translationPopup.textContent = translatedText;
translationPopup.style.left = x + 'px';
translationPopup.style.top = (y + 20) + 'px';
document.body.appendChild(translationPopup);
// 点击其他地方关闭翻译框
document.addEventListener('click', function closePopup(e) {
if (e.target !== translationPopup) {
translationPopup.remove();
document.removeEventListener('click', closePopup);
}
});
}
function cleanup() {
isSelecting = false;
const overlay = document.querySelector('.page-overlay');
if (overlay) {
overlay.remove();
}
if (selectionElement) {
selectionElement.remove();
selectionElement = null;
}
startX = startY = null;
document.removeEventListener('mousedown', handleMouseDown);
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
document.removeEventListener('keydown', handleKeyDown);
}
function handleKeyDown(e) {
// 检测 Mac 和 Windows 的按键
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
const triggerKey = isMac ? e.metaKey : e.ctrlKey;
if (e.key === 'Escape' || (triggerKey && e.key === 'q')) {
cleanup();
}
}
// 添加 MD5 函数
function md5(string) {
function md5_RotateLeft(lValue, iShiftBits) {
return (lValue << iShiftBits) | (lValue >>> (32 - iShiftBits));
}
function md5_AddUnsigned(lX, lY) {
var lX4, lY4, lX8, lY8, lResult;
lX8 = (lX & 0x80000000);
lY8 = (lY & 0x80000000);
lX4 = (lX & 0x40000000);
lY4 = (lY & 0x40000000);
lResult = (lX & 0x3FFFFFFF) + (lY & 0x3FFFFFFF);
if (lX4 & lY4) {
return (lResult ^ 0x80000000 ^ lX8 ^ lY8);
}
if (lX4 | lY4) {
if (lResult & 0x40000000) {
return (lResult ^ 0xC0000000 ^ lX8 ^ lY8);
} else {
return (lResult ^ 0x40000000 ^ lX8 ^ lY8);
}
} else {
return (lResult ^ lX8 ^ lY8);
}
}
function md5_F(x, y, z) {
return (x & y) | ((~x) & z);
}
function md5_G(x, y, z) {
return (x & z) | (y & (~z));
}
function md5_H(x, y, z) {
return (x ^ y ^ z);
}
function md5_I(x, y, z) {
return (y ^ (x | (~z)));
}
function md5_FF(a, b, c, d, x, s, ac) {
a = md5_AddUnsigned(a, md5_AddUnsigned(md5_AddUnsigned(md5_F(b, c, d), x), ac));
return md5_AddUnsigned(md5_RotateLeft(a, s), b);
}
function md5_GG(a, b, c, d, x, s, ac) {
a = md5_AddUnsigned(a, md5_AddUnsigned(md5_AddUnsigned(md5_G(b, c, d), x), ac));
return md5_AddUnsigned(md5_RotateLeft(a, s), b);
}
function md5_HH(a, b, c, d, x, s, ac) {
a = md5_AddUnsigned(a, md5_AddUnsigned(md5_AddUnsigned(md5_H(b, c, d), x), ac));
return md5_AddUnsigned(md5_RotateLeft(a, s), b);
}
function md5_II(a, b, c, d, x, s, ac) {
a = md5_AddUnsigned(a, md5_AddUnsigned(md5_AddUnsigned(md5_I(b, c, d), x), ac));
return md5_AddUnsigned(md5_RotateLeft(a, s), b);
}
function md5_ConvertToWordArray(string) {
var lWordCount;
var lMessageLength = string.length;
var lNumberOfWords_temp1 = lMessageLength + 8;
var lNumberOfWords_temp2 = (lNumberOfWords_temp1 - (lNumberOfWords_temp1 % 64)) / 64;
var lNumberOfWords = (lNumberOfWords_temp2 + 1) * 16;
var lWordArray = Array(lNumberOfWords - 1);
var lBytePosition = 0;
var lByteCount = 0;
while (lByteCount < lMessageLength) {
lWordCount = (lByteCount - (lByteCount % 4)) / 4;
lBytePosition = (lByteCount % 4) * 8;
lWordArray[lWordCount] = (lWordArray[lWordCount] | (string.charCodeAt(lByteCount) << lBytePosition));
lByteCount++;
}
lWordCount = (lByteCount - (lByteCount % 4)) / 4;
lBytePosition = (lByteCount % 4) * 8;
lWordArray[lWordCount] = lWordArray[lWordCount] | (0x80 << lBytePosition);
lWordArray[lNumberOfWords - 2] = lMessageLength << 3;
lWordArray[lNumberOfWords - 1] = lMessageLength >>> 29;
return lWordArray;
}
function md5_WordToHex(lValue) {
var WordToHexValue = "", WordToHexValue_temp = "", lByte, lCount;
for (lCount = 0; lCount <= 3; lCount++) {
lByte = (lValue >>> (lCount * 8)) & 255;
WordToHexValue_temp = "0" + lByte.toString(16);
WordToHexValue = WordToHexValue + WordToHexValue_temp.substr(WordToHexValue_temp.length - 2, 2);
}
return WordToHexValue;
}
function md5_Utf8Encode(string) {
string = string.replace(/\r\n/g, "\n");
var utftext = "";
for (var n = 0; n < string.length; n++) {
var c = string.charCodeAt(n);
if (c < 128) {
utftext += String.fromCharCode(c);
} else if ((c > 127) && (c < 2048)) {
utftext += String.fromCharCode((c >> 6) | 192);
utftext += String.fromCharCode((c & 63) | 128);
} else {
utftext += String.fromCharCode((c >> 12) | 224);
utftext += String.fromCharCode(((c >> 6) & 63) | 128);
utftext += String.fromCharCode((c & 63) | 128);
}
}
return utftext;
}
var x = Array();
var k, AA, BB, CC, DD, a, b, c, d;
var S11 = 7, S12 = 12, S13 = 17, S14 = 22;
var S21 = 5, S22 = 9, S23 = 14, S24 = 20;
var S31 = 4, S32 = 11, S33 = 16, S34 = 23;
var S41 = 6, S42 = 10, S43 = 15, S44 = 21;
string = md5_Utf8Encode(string);
x = md5_ConvertToWordArray(string);
a = 0x67452301; b = 0xEFCDAB89; c = 0x98BADCFE; d = 0x10325476;
for (k = 0; k < x.length; k += 16) {
AA = a; BB = b; CC = c; DD = d;
a = md5_FF(a, b, c, d, x[k + 0], S11, 0xD76AA478);
d = md5_FF(d, a, b, c, x[k + 1], S12, 0xE8C7B756);
c = md5_FF(c, d, a, b, x[k + 2], S13, 0x242070DB);
b = md5_FF(b, c, d, a, x[k + 3], S14, 0xC1BDCEEE);
a = md5_FF(a, b, c, d, x[k + 4], S11, 0xF57C0FAF);
d = md5_FF(d, a, b, c, x[k + 5], S12, 0x4787C62A);
c = md5_FF(c, d, a, b, x[k + 6], S13, 0xA8304613);
b = md5_FF(b, c, d, a, x[k + 7], S14, 0xFD469501);
a = md5_FF(a, b, c, d, x[k + 8], S11, 0x698098D8);
d = md5_FF(d, a, b, c, x[k + 9], S12, 0x8B44F7AF);
c = md5_FF(c, d, a, b, x[k + 10], S13, 0xFFFF5BB1);
b = md5_FF(b, c, d, a, x[k + 11], S14, 0x895CD7BE);
a = md5_FF(a, b, c, d, x[k + 12], S11, 0x6B901122);
d = md5_FF(d, a, b, c, x[k + 13], S12, 0xFD987193);
c = md5_FF(c, d, a, b, x[k + 14], S13, 0xA679438E);
b = md5_FF(b, c, d, a, x[k + 15], S14, 0x49B40821);
a = md5_GG(a, b, c, d, x[k + 1], S21, 0xF61E2562);
d = md5_GG(d, a, b, c, x[k + 6], S22, 0xC040B340);
c = md5_GG(c, d, a, b, x[k + 11], S23, 0x265E5A51);
b = md5_GG(b, c, d, a, x[k + 0], S24, 0xE9B6C7AA);
a = md5_GG(a, b, c, d, x[k + 5], S21, 0xD62F105D);
d = md5_GG(d, a, b, c, x[k + 10], S22, 0x2441453);
c = md5_GG(c, d, a, b, x[k + 15], S23, 0xD8A1E681);
b = md5_GG(b, c, d, a, x[k + 4], S24, 0xE7D3FBC8);
a = md5_GG(a, b, c, d, x[k + 9], S21, 0x21E1CDE6);
d = md5_GG(d, a, b, c, x[k + 14], S22, 0xC33707D6);
c = md5_GG(c, d, a, b, x[k + 3], S23, 0xF4D50D87);
b = md5_GG(b, c, d, a, x[k + 8], S24, 0x455A14ED);
a = md5_GG(a, b, c, d, x[k + 13], S21, 0xA9E3E905);
d = md5_GG(d, a, b, c, x[k + 2], S22, 0xFCEFA3F8);
c = md5_GG(c, d, a, b, x[k + 7], S23, 0x676F02D9);
b = md5_GG(b, c, d, a, x[k + 12], S24, 0x8D2A4C8A);
a = md5_HH(a, b, c, d, x[k + 5], S31, 0xFFFA3942);
d = md5_HH(d, a, b, c, x[k + 8], S32, 0x8771F681);
c = md5_HH(c, d, a, b, x[k + 11], S33, 0x6D9D6122);
b = md5_HH(b, c, d, a, x[k + 14], S34, 0xFDE5380C);
a = md5_HH(a, b, c, d, x[k + 1], S31, 0xA4BEEA44);
d = md5_HH(d, a, b, c, x[k + 4], S32, 0x4BDECFA9);
c = md5_HH(c, d, a, b, x[k + 7], S33, 0xF6BB4B60);
b = md5_HH(b, c, d, a, x[k + 10], S34, 0xBEBFBC70);
a = md5_HH(a, b, c, d, x[k + 13], S31, 0x289B7EC6);
d = md5_HH(d, a, b, c, x[k + 0], S32, 0xEAA127FA);
c = md5_HH(c, d, a, b, x[k + 3], S33, 0xD4EF3085);
b = md5_HH(b, c, d, a, x[k + 6], S34, 0x4881D05);
a = md5_HH(a, b, c, d, x[k + 9], S31, 0xD9D4D039);
d = md5_HH(d, a, b, c, x[k + 12], S32, 0xE6DB99E5);
c = md5_HH(c, d, a, b, x[k + 15], S33, 0x1FA27CF8);
b = md5_HH(b, c, d, a, x[k + 2], S34, 0xC4AC5665);
a = md5_II(a, b, c, d, x[k + 0], S41, 0xF4292244);
d = md5_II(d, a, b, c, x[k + 7], S42, 0x432AFF97);
c = md5_II(c, d, a, b, x[k + 14], S43, 0xAB9423A7);
b = md5_II(b, c, d, a, x[k + 5], S44, 0xFC93A039);
a = md5_II(a, b, c, d, x[k + 12], S41, 0x655B59C3);
d = md5_II(d, a, b, c, x[k + 3], S42, 0x8F0CCC92);
c = md5_II(c, d, a, b, x[k + 10], S43, 0xFFEFF47D);
b = md5_II(b, c, d, a, x[k + 1], S44, 0x85845DD1);
a = md5_II(a, b, c, d, x[k + 8], S41, 0x6FA87E4F);
d = md5_II(d, a, b, c, x[k + 15], S42, 0xFE2CE6E0);
c = md5_II(c, d, a, b, x[k + 6], S43, 0xA3014314);
b = md5_II(b, c, d, a, x[k + 13], S44, 0x4E0811A1);
a = md5_II(a, b, c, d, x[k + 4], S41, 0xF7537E82);
d = md5_II(d, a, b, c, x[k + 11], S42, 0xBD3AF235);
c = md5_II(c, d, a, b, x[k + 2], S43, 0x2AD7D2BB);
b = md5_II(b, c, d, a, x[k + 9], S44, 0xEB86D391);
a = md5_AddUnsigned(a, AA);
b = md5_AddUnsigned(b, BB);
c = md5_AddUnsigned(c, CC);
d = md5_AddUnsigned(d, DD);
}
return (md5_WordToHex(a) + md5_WordToHex(b) + md5_WordToHex(c) + md5_WordToHex(d)).toLowerCase();
}

View File

@ -9,12 +9,40 @@
"128": "icons/icon128.png"
},
"action": {
"default_popup": "src/html/popup.html"
"default_popup": "popup.html",
"default_icon": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}
},
"permissions": [
"activeTab"
"activeTab",
"downloads",
"scripting",
"storage",
"tabs"
],
"host_permissions": [
"http://localhost:5000/*"
]
"https://api.fanyi.baidu.com/*",
"<all_urls>"
],
"commands": {
"trigger-select": {
"suggested_key": {
"default": "Ctrl+Q",
"mac": "Command+Q"
},
"description": "触发框选功能"
}
},
"background": {
"service_worker": "background.js",
"type": "module"
},
"content_scripts": [{
"matches": ["<all_urls>"],
"js": ["md5.js", "content.js"],
"css": ["content.css"]
}]
}

19
md5.js Normal file
View File

@ -0,0 +1,19 @@
/*
CryptoJS v3.1.2
code.google.com/p/crypto-js
(c) 2009-2013 by Jeff Mott. All rights reserved.
code.google.com/p/crypto-js/wiki/License
*/
var CryptoJS=CryptoJS||function(s,p){var m={},l=m.lib={},n=function(){},r=l.Base={extend:function(b){n.prototype=this;var h=new n;b&&h.mixIn(b);h.hasOwnProperty("init")||(h.init=function(){h.$super.init.apply(this,arguments)});h.init.prototype=h;h.$super=this;return h},create:function(){var b=this.extend();b.init.apply(b,arguments);return b},init:function(){},mixIn:function(b){for(var h in b)b.hasOwnProperty(h)&&(this[h]=b[h]);b.hasOwnProperty("toString")&&(this.toString=b.toString)},clone:function(){return this.init.prototype.extend(this)}},
q=l.WordArray=r.extend({init:function(b,h){b=this.words=b||[];this.sigBytes=h!=p?h:4*b.length},toString:function(b){return(b||t).stringify(this)},concat:function(b){var h=this.words,a=b.words,j=this.sigBytes;b=b.sigBytes;this.clamp();if(j%4)for(var g=0;g<b;g++)h[j+g>>>2]|=(a[g>>>2]>>>24-8*(g%4)&255)<<24-8*((j+g)%4);else if(65535<a.length)for(g=0;g<b;g+=4)h[j+g>>>2]=a[g>>>2];else h.push.apply(h,a);this.sigBytes+=b;return this},clamp:function(){var b=this.words,h=this.sigBytes;b[h>>>2]&=4294967295<<
32-8*(h%4);b.length=s.ceil(h/4)},clone:function(){var b=r.clone.call(this);b.words=this.words.slice(0);return b},random:function(b){for(var h=[],a=0;a<b;a+=4)h.push(4294967296*s.random()|0);return new q.init(h,b)}}),v=m.enc={},t=v.Hex={stringify:function(b){var a=b.words;b=b.sigBytes;for(var g=[],j=0;j<b;j++){var k=a[j>>>2]>>>24-8*(j%4)&255;g.push((k>>>4).toString(16));g.push((k&15).toString(16))}return g.join("")},parse:function(b){for(var a=b.length,g=[],j=0;j<a;j+=2)g[j>>>3]|=parseInt(b.substr(j,
2),16)<<24-4*(j%8);return new q.init(g,a/2)}},a=v.Latin1={stringify:function(b){var a=b.words;b=b.sigBytes;for(var g=[],j=0;j<b;j++)g.push(String.fromCharCode(a[j>>>2]>>>24-8*(j%4)&255));return g.join("")},parse:function(b){for(var a=b.length,g=[],j=0;j<a;j++)g[j>>>2]|=(b.charCodeAt(j)&255)<<24-8*(j%4);return new q.init(g,a)}},u=v.Utf8={stringify:function(b){try{return decodeURIComponent(escape(a.stringify(b)))}catch(g){throw Error("Malformed UTF-8 data");}},parse:function(b){return a.parse(unescape(encodeURIComponent(b)))}},
g=l.BufferedBlockAlgorithm=r.extend({reset:function(){this._data=new q.init;this._nDataBytes=0},_append:function(b){"string"==typeof b&&(b=u.parse(b));this._data.concat(b);this._nDataBytes+=b.sigBytes},_process:function(b){var a=this._data,g=a.words,j=a.sigBytes,k=this.blockSize,m=j/(4*k),m=b?s.ceil(m):s.max((m|0)-this._minBufferSize,0);b=m*k;j=s.min(4*b,j);if(b){for(var l=0;l<b;l+=k)this._doProcessBlock(g,l);l=g.splice(0,b);a.sigBytes-=j}return new q.init(l,j)},clone:function(){var b=r.clone.call(this);
b._data=this._data.clone();return b},_minBufferSize:0});l.Hasher=g.extend({cfg:r.extend(),init:function(b){this.cfg=this.cfg.extend(b);this.reset()},reset:function(){g.reset.call(this);this._doReset()},update:function(b){this._append(b);this._process();return this},finalize:function(b){b&&this._append(b);return this._doFinalize()},blockSize:16,_createHelper:function(b){return function(a,g){return(new b.init(g)).finalize(a)}},_createHmacHelper:function(b){return function(a,g){return(new k.HMAC.init(b,
g)).finalize(a)}}});var k=m.algo={};return m}(Math);
(function(s){function p(a,k,b,h,l,j,m){a=a+(k&b|~k&h)+l+m;return(a<<j|a>>>32-j)+k}function m(a,k,b,h,l,j,m){a=a+(k&h|b&~h)+l+m;return(a<<j|a>>>32-j)+k}function l(a,k,b,h,l,j,m){a=a+(k^b^h)+l+m;return(a<<j|a>>>32-j)+k}function n(a,k,b,h,l,j,m){a=a+(b^(k|~h))+l+m;return(a<<j|a>>>32-j)+k}for(var r=CryptoJS,q=r.lib,v=q.WordArray,t=q.Hasher,q=r.algo,a=[],u=0;64>u;u++)a[u]=4294967296*s.abs(s.sin(u+1))|0;q=q.MD5=t.extend({_doReset:function(){this._hash=new v.init([1732584193,4023233417,2562383102,271733878])},
_doProcessBlock:function(g,k){for(var b=0;16>b;b++){var h=k+b,w=g[h];g[h]=(w<<8|w>>>24)&16711935|(w<<24|w>>>8)&4278255360}var b=this._hash.words,h=g[k+0],w=g[k+1],j=g[k+2],q=g[k+3],r=g[k+4],s=g[k+5],t=g[k+6],u=g[k+7],v=g[k+8],x=g[k+9],y=g[k+10],z=g[k+11],A=g[k+12],B=g[k+13],C=g[k+14],D=g[k+15],c=b[0],d=b[1],e=b[2],f=b[3],c=p(c,d,e,f,h,7,a[0]),f=p(f,c,d,e,w,12,a[1]),e=p(e,f,c,d,j,17,a[2]),d=p(d,e,f,c,q,22,a[3]),c=p(c,d,e,f,r,7,a[4]),f=p(f,c,d,e,s,12,a[5]),e=p(e,f,c,d,t,17,a[6]),d=p(d,e,f,c,u,22,a[7]),
c=p(c,d,e,f,v,7,a[8]),f=p(f,c,d,e,x,12,a[9]),e=p(e,f,c,d,y,17,a[10]),d=p(d,e,f,c,z,22,a[11]),c=p(c,d,e,f,A,7,a[12]),f=p(f,c,d,e,B,12,a[13]),e=p(e,f,c,d,C,17,a[14]),d=p(d,e,f,c,D,22,a[15]),c=m(c,d,e,f,w,5,a[16]),f=m(f,c,d,e,t,9,a[17]),e=m(e,f,c,d,z,14,a[18]),d=m(d,e,f,c,h,20,a[19]),c=m(c,d,e,f,s,5,a[20]),f=m(f,c,d,e,y,9,a[21]),e=m(e,f,c,d,D,14,a[22]),d=m(d,e,f,c,r,20,a[23]),c=m(c,d,e,f,x,5,a[24]),f=m(f,c,d,e,C,9,a[25]),e=m(e,f,c,d,q,14,a[26]),d=m(d,e,f,c,v,20,a[27]),c=m(c,d,e,f,B,5,a[28]),f=m(f,c,
d,e,j,9,a[29]),e=m(e,f,c,d,u,14,a[30]),d=m(d,e,f,c,A,20,a[31]),c=l(c,d,e,f,s,4,a[32]),f=l(f,c,d,e,v,11,a[33]),e=l(e,f,c,d,z,16,a[34]),d=l(d,e,f,c,C,23,a[35]),c=l(c,d,e,f,w,4,a[36]),f=l(f,c,d,e,r,11,a[37]),e=l(e,f,c,d,u,16,a[38]),d=l(d,e,f,c,y,23,a[39]),c=l(c,d,e,f,B,4,a[40]),f=l(f,c,d,e,h,11,a[41]),e=l(e,f,c,d,q,16,a[42]),d=l(d,e,f,c,t,23,a[43]),c=l(c,d,e,f,x,4,a[44]),f=l(f,c,d,e,A,11,a[45]),e=l(e,f,c,d,D,16,a[46]),d=l(d,e,f,c,j,23,a[47]),c=n(c,d,e,f,h,6,a[48]),f=n(f,c,d,e,u,10,a[49]),e=n(e,f,c,d,
C,15,a[50]),d=n(d,e,f,c,s,21,a[51]),c=n(c,d,e,f,A,6,a[52]),f=n(f,c,d,e,q,10,a[53]),e=n(e,f,c,d,y,15,a[54]),d=n(d,e,f,c,w,21,a[55]),c=n(c,d,e,f,v,6,a[56]),f=n(f,c,d,e,D,10,a[57]),e=n(e,f,c,d,t,15,a[58]),d=n(d,e,f,c,B,21,a[59]),c=n(c,d,e,f,r,6,a[60]),f=n(f,c,d,e,z,10,a[61]),e=n(e,f,c,d,j,15,a[62]),d=n(d,e,f,c,x,21,a[63]);b[0]=b[0]+c|0;b[1]=b[1]+d|0;b[2]=b[2]+e|0;b[3]=b[3]+f|0},_doFinalize:function(){var a=this._data,k=a.words,b=8*this._nDataBytes,h=8*a.sigBytes;k[h>>>5]|=128<<24-h%32;var l=s.floor(b/
4294967296);k[(h+64>>>9<<4)+15]=(l<<8|l>>>24)&16711935|(l<<24|l>>>8)&4278255360;k[(h+64>>>9<<4)+14]=(b<<8|b>>>24)&16711935|(b<<24|b>>>8)&4278255360;a.sigBytes=4*(k.length+1);this._process();a=this._hash;k=a.words;for(b=0;4>b;b++)h=k[b],k[b]=(h<<8|h>>>24)&16711935|(h<<24|h>>>8)&4278255360;return a},clone:function(){var a=t.clone.call(this);a._hash=this._hash.clone();return a}});r.MD5=t._createHelper(q);r.HmacMD5=t._createHmacHelper(q)})(Math);

View File

@ -2,9 +2,9 @@
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="../css/styles.css">
<script src="../../lib/qrcode.min.js"></script>
<script src="../../lib/mammoth.browser.min.js"></script>
<link rel="stylesheet" href="styles.css">
<script src="qrcode.min.js"></script>
<script src="mammoth.browser.min.js"></script>
</head>
<body>
<div class="container">
@ -12,12 +12,15 @@
<div class="tab">
<button class="tablinks active" data-tab="qr">
<span class="icon">🔲</span>
二维码生成
</button>
<button class="tablinks" data-tab="word">
<span class="icon">📄</span>
Word转HTML
</button>
<button class="tablinks" data-tab="translate">
<span class="icon">🔄</span>
<span class="icon">🔤</span>
翻译设置
</button>
</div>
@ -46,31 +49,24 @@
</div>
<div id="translate" class="tabcontent" style="display:none;">
<div class="file-upload">
<label for="wordTranslateFile" class="file-label">
<span class="icon">📎</span>
<span id="translateFileNameDisplay">选择Word文件</span>
<div class="translate-settings">
<div class="setting-item">
<label class="switch">
<input type="checkbox" id="translateEnabled">
<span class="slider"></span>
</label>
<input type="file" id="wordTranslateFile" accept=".docx" hidden>
<span class="setting-label">启用翻译功能 (Ctrl+Q)</span>
</div>
<div class="language-select">
<div class="setting-item">
<label>目标语言:</label>
<select id="targetLang">
<option value="en">英文</option>
<option value="jp">日文</option>
<option value="cht">繁体中文</option>
</select>
</div>
<div id="translateProgress" class="progress-container">
<div class="progress-bar"></div>
</div>
<div id="progressText" class="progress-text"></div>
<div id="translateStatus" class="status-message"></div>
<button id="translateBtn" class="translate-btn" style="display:none;">
<span class="icon">🔄</span>
开始翻译
</button>
</div>
</div>
<script src="../js/popup.js"></script>
</div>
<script src="popup.js"></script>
</body>
</html>

174
popup.js Normal file
View File

@ -0,0 +1,174 @@
document.addEventListener('DOMContentLoaded', function() {
const urlInput = document.getElementById('urlInput');
const generateBtn = document.getElementById('generateBtn');
const qrcodeDiv = document.getElementById('qrcode');
const wordFile = document.getElementById('wordFile');
const preview = document.getElementById('preview');
const downloadBtn = document.getElementById('downloadBtn');
let qr = null;
let convertedHtml = '';
// 加载翻译设置
loadTranslationSettings();
const tabs = document.querySelectorAll('.tablinks');
const tabContents = document.querySelectorAll('.tabcontent');
tabs.forEach(tab => {
tab.addEventListener('click', (e) => {
const tabName = e.currentTarget.getAttribute('data-tab');
tabs.forEach(t => t.classList.remove('active'));
tab.classList.add('active');
tabContents.forEach(content => {
content.style.display = 'none';
});
const selectedContent = document.getElementById(tabName);
if (selectedContent) {
selectedContent.style.display = 'block';
}
});
});
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
if (tabs[0] && tabs[0].url) {
urlInput.value = tabs[0].url;
generateQRCode(tabs[0].url);
}
});
generateBtn.addEventListener('click', function() {
generateQRCode(urlInput.value);
});
function generateQRCode(text) {
if (!text) return;
qrcodeDiv.innerHTML = '';
qr = new QRCode(qrcodeDiv, {
text: text,
width: 256,
height: 256,
colorDark: "#000000",
colorLight: "#ffffff",
correctLevel: QRCode.CorrectLevel.H
});
}
// Word转HTML功能
const fileNameDisplay = document.getElementById('fileNameDisplay');
const convertStatus = document.getElementById('convertStatus');
let currentFileName = '';
wordFile.addEventListener('change', function(e) {
const file = e.target.files[0];
if (file) {
currentFileName = file.name.replace(/\.docx$/i, '');
fileNameDisplay.innerHTML = `
<span class="file-name" title="${file.name}">
📄 ${file.name}
</span>
`;
showStatus('loading', '正在转换中...');
const reader = new FileReader();
reader.onload = function(e) {
mammoth.convertToHtml({arrayBuffer: e.target.result})
.then(function(result) {
convertedHtml = result.value;
preview.innerHTML = convertedHtml;
downloadBtn.style.display = 'block';
showStatus('success', '转换成功!');
})
.catch(function(error) {
console.error(error);
showStatus('error', '转换失败:' + error.message);
});
};
reader.readAsArrayBuffer(file);
}
});
function showStatus(type, message) {
convertStatus.className = 'status-message ' + type;
convertStatus.textContent = message;
if (type === 'success') {
setTimeout(() => {
convertStatus.style.display = 'none';
}, 3000);
}
}
downloadBtn.addEventListener('click', function() {
if (!convertedHtml) return;
const fullHtml = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>${currentFileName}</title>
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; }
</style>
</head>
<body>
${convertedHtml}
</body>
</html>
`;
const blob = new Blob([fullHtml], {type: 'text/html'});
const url = URL.createObjectURL(blob);
chrome.downloads.download({
url: url,
filename: `${currentFileName}.html`,
saveAs: true
});
});
// 翻译设置相关
function loadTranslationSettings() {
const translateEnabled = document.getElementById('translateEnabled');
const targetLang = document.getElementById('targetLang');
// 检测操作系统
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
const shortcutText = isMac ? 'Command+Q' : 'Ctrl+Q';
// 更新显示的快捷键文本
const settingLabel = document.querySelector('.setting-label');
if (settingLabel) {
settingLabel.textContent = `启用翻译功能 (${shortcutText})`;
}
// 加载保存的设置
chrome.storage.sync.get(['translateEnabled', 'targetLang'], function(result) {
translateEnabled.checked = result.translateEnabled !== false; // 默认为 true
targetLang.value = result.targetLang || 'en';
});
// 监听设置变更
translateEnabled.addEventListener('change', function(e) {
const enabled = e.target.checked;
chrome.storage.sync.set({ translateEnabled: enabled }, function() {
console.log('Translation enabled:', enabled);
});
});
targetLang.addEventListener('change', function(e) {
const lang = e.target.value;
chrome.storage.sync.set({ targetLang: lang }, function() {
console.log('Target language:', lang);
});
});
}
});

View File

View File

@ -1,272 +0,0 @@
document.addEventListener('DOMContentLoaded', function() {
const urlInput = document.getElementById('urlInput');
const generateBtn = document.getElementById('generateBtn');
const qrcodeDiv = document.getElementById('qrcode');
const wordFile = document.getElementById('wordFile');
const preview = document.getElementById('preview');
const downloadBtn = document.getElementById('downloadBtn');
let qr = null;
let convertedHtml = '';
const tabs = document.querySelectorAll('.tablinks');
const tabContents = document.querySelectorAll('.tabcontent');
function switchTab(tabName) {
// 移除所有标签的激活状态
tabs.forEach(tab => tab.classList.remove('active'));
// 隐藏所有内容
tabContents.forEach(content => {
content.style.display = 'none';
});
// 激活选中的标签
const selectedTab = document.querySelector(`[data-tab="${tabName}"]`);
if (selectedTab) {
selectedTab.classList.add('active');
}
// 显示对应内容
const selectedContent = document.getElementById(tabName);
if (selectedContent) {
selectedContent.style.display = 'block';
}
}
// 为每个标签添加点击事件
tabs.forEach(tab => {
tab.addEventListener('click', (e) => {
const tabName = e.currentTarget.getAttribute('data-tab');
if (tabName) {
switchTab(tabName);
}
});
});
// 初始化显示第一个标签页
switchTab('qr');
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
if (tabs[0] && tabs[0].url) {
urlInput.value = tabs[0].url;
generateQRCode(tabs[0].url);
}
});
generateBtn.addEventListener('click', function() {
generateQRCode(urlInput.value);
});
function generateQRCode(text) {
if (!text) return;
qrcodeDiv.innerHTML = '';
qr = new QRCode(qrcodeDiv, {
text: text,
width: 256,
height: 256,
colorDark: "#000000",
colorLight: "#ffffff",
correctLevel: QRCode.CorrectLevel.H
});
}
// Word转HTML功能
const fileNameDisplay = document.getElementById('fileNameDisplay');
const convertStatus = document.getElementById('convertStatus');
let currentFileName = '';
wordFile.addEventListener('change', function(e) {
const file = e.target.files[0];
if (file) {
currentFileName = file.name.replace(/\.docx$/i, '');
fileNameDisplay.innerHTML = `
<span class="file-name" title="${file.name}">
📄 ${file.name}
</span>
`;
showStatus('loading', '正在转换中...');
const reader = new FileReader();
reader.onload = function(e) {
mammoth.convertToHtml({arrayBuffer: e.target.result})
.then(function(result) {
convertedHtml = result.value;
preview.innerHTML = convertedHtml;
downloadBtn.style.display = 'block';
showStatus('success', '转换成功!');
})
.catch(function(error) {
console.error(error);
showStatus('error', '转换失败:' + error.message);
});
};
reader.readAsArrayBuffer(file);
}
});
function showStatus(type, message) {
convertStatus.className = 'status-message ' + type;
convertStatus.textContent = message;
if (type === 'success') {
setTimeout(() => {
convertStatus.style.display = 'none';
}, 3000);
}
}
downloadBtn.addEventListener('click', function() {
if (!convertedHtml) return;
const fullHtml = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>${currentFileName}</title>
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; }
</style>
</head>
<body>
${convertedHtml}
</body>
</html>
`;
const blob = new Blob([fullHtml], {type: 'text/html'});
const url = URL.createObjectURL(blob);
chrome.downloads.download({
url: url,
filename: `${currentFileName}.html`,
saveAs: true
});
});
// 添加百度翻译API配置
const BAIDU_APPID = '20241112002200806';
const BAIDU_KEY = 'preM0becByYCdotRTP_a';
let wordTranslateFile = null;
document.getElementById('wordTranslateFile').addEventListener('change', async (e) => {
const file = e.target.files[0];
if (file) {
wordTranslateFile = file;
document.getElementById('translateFileNameDisplay').textContent = file.name;
document.getElementById('translateBtn').style.display = 'block';
}
});
document.getElementById('translateBtn').addEventListener('click', async () => {
if (!wordTranslateFile) return;
const translateBtn = document.getElementById('translateBtn');
const translateStatus = document.getElementById('translateStatus');
const progressBar = document.querySelector('.progress-bar');
const progressContainer = document.querySelector('.progress-container');
const progressText = document.getElementById('progressText');
// 禁用翻译按钮
translateBtn.disabled = true;
translateBtn.style.opacity = '0.6';
translateBtn.textContent = '翻译中...';
translateStatus.textContent = '正在处理文档...';
progressContainer.style.display = 'block';
progressText.style.display = 'block';
progressBar.style.width = '0%';
// 进度检查变量
let lastProgress = 0;
let noProgressCount = 0;
try {
const formData = new FormData();
formData.append('file', wordTranslateFile);
formData.append('target_lang', document.getElementById('targetLang').value);
// 开始翻译请求
const response = await fetch('http://172.16.100.22:8044/translate', {
method: 'POST',
body: formData
});
// 定时检查进度
const progressCheck = setInterval(async () => {
try {
const progressResponse = await fetch('http://172.16.100.22:8044/progress');
const progressData = await progressResponse.json();
// 检查进度是否停滞
if (progressData.progress === lastProgress) {
noProgressCount++;
if (noProgressCount > 10) { // 5秒无进度更新
clearInterval(progressCheck);
throw new Error('翻译进度停滞,请重试');
}
} else {
noProgressCount = 0;
lastProgress = progressData.progress;
}
progressBar.style.width = `${progressData.progress}%`;
progressText.textContent = `翻译进度: ${progressData.progress}%`;
} catch (error) {
clearInterval(progressCheck);
throw error;
}
}, 500);
if (!response.ok) {
clearInterval(progressCheck);
const error = await response.json();
throw new Error(error.error || '翻译失败');
}
// 下载翻译后的文件
const blob = await response.blob();
clearInterval(progressCheck);
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `translated_${wordTranslateFile.name}`;
a.click();
translateStatus.textContent = '翻译完成!';
progressBar.style.width = '100%';
// 重置按钮状态
setTimeout(() => {
translateBtn.disabled = false;
translateBtn.style.opacity = '1';
translateBtn.innerHTML = '<span class="icon">🔄</span>开始翻译';
progressContainer.style.display = 'none';
progressText.style.display = 'none';
}, 2000);
} catch (error) {
translateStatus.textContent = '翻译失败:' + error.message;
progressContainer.style.display = 'none';
progressText.style.display = 'none';
// 重置按钮状态
translateBtn.disabled = false;
translateBtn.style.opacity = '1';
translateBtn.innerHTML = '<span class="icon">🔄</span>开始翻译';
}
});
// MD5函数实现
function MD5(string) {
// 实现MD5加密
// 可以使用第三方库如crypto-js
}
});

View File

@ -10,7 +10,7 @@ body {
}
.container {
width: 380px;
width: 400px;
padding: 20px;
background: white;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
@ -25,34 +25,31 @@ h1 {
}
.tab {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 5px;
display: flex;
gap: 8px;
margin-bottom: 20px;
background: #f5f5f5;
padding: 5px;
border-radius: 8px;
min-height: 45px;
}
.tab button {
flex: 1;
padding: 8px 4px;
font-size: 12px;
white-space: nowrap;
min-width: 0;
border: none;
background: transparent;
color: #666;
cursor: pointer;
border-radius: 6px;
transition: all 0.3s ease;
font-size: 13px;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
}
.tab button .icon {
font-size: 14px;
white-space: nowrap;
min-width: 80px;
}
.tab button:hover {
@ -154,7 +151,8 @@ button:hover {
}
.icon {
font-size: 18px;
font-size: 16px;
flex-shrink: 0;
}
/* 美化滚动条 */
@ -209,10 +207,11 @@ button:hover {
.status-message {
margin: 10px 0;
padding: 8px 12px;
padding: 10px;
border-radius: 6px;
font-size: 13px;
text-align: center;
font-size: 14px;
display: none;
animation: fadeIn 0.3s ease;
}
.status-message.success {
@ -248,80 +247,71 @@ button:hover {
white-space: nowrap;
}
/* 添加新的样式 */
.language-select {
margin: 10px 0;
/* 翻译设置样式 */
.translate-settings {
padding: 15px;
}
.language-select select {
width: 100%;
padding: 8px;
border-radius: 4px;
border: 1px solid #ccc;
.setting-item {
margin-bottom: 15px;
display: flex;
align-items: center;
}
.translate-btn {
width: 100%;
padding: 10px;
background: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
margin-top: 10px;
.setting-label {
margin-left: 10px;
}
.translate-btn:hover {
background: #45a049;
/* 开关按钮样式 */
.switch {
position: relative;
display: inline-block;
width: 50px;
height: 24px;
}
/* 添加进度条样式 */
.progress-container {
margin: 15px 0;
background: #f0f0f0;
border-radius: 4px;
overflow: hidden;
height: 4px;
display: none;
}
.progress-bar {
height: 100%;
background: #1a73e8;
.switch input {
opacity: 0;
width: 0;
transition: width 0.3s ease;
height: 0;
}
/* 添加进度文本样式 */
.progress-text {
font-size: 12px;
color: #666;
text-align: center;
margin-top: 5px;
display: none;
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
border-radius: 24px;
}
/* 修改选项卡文字样式 */
.tab button[data-tab="qr"] {
font-size: 12px;
.slider:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
.tab button[data-tab="qr"]::after {
content: "二维码生成";
input:checked + .slider {
background-color: #2196F3;
}
.tab button[data-tab="word"] {
font-size: 12px;
input:checked + .slider:before {
transform: translateX(26px);
}
.tab button[data-tab="word"]::after {
content: "Word转HTML";
}
.tab button[data-tab="translate"] {
font-size: 12px;
}
.tab button[data-tab="translate"]::after {
content: "Word翻译";
/* 下拉菜单样式 */
select {
padding: 5px;
border-radius: 4px;
border: 1px solid #ddd;
margin-left: 10px;
}