`)
}
// 过滤 HTML,防止XSS
function HTMLEncode(s) {
let d = document.createElement('div')
d.textContent = s
return d.innerHTML || ''
}
function uniqueArray(arr) {
return [...new Set(arr)]
}
function httpGet(url, type, headers, notStrict) {
return new Promise((resolve, reject) => {
let c = new XMLHttpRequest()
c.responseType = type || 'text'
c.timeout = 20000
c.onload = function (e) {
if (notStrict) {
resolve(this.response)
} else {
if (this.status === 200) {
resolve(this.response)
} else {
reject(e)
}
}
}
c.ontimeout = function (e) {
reject(e)
}
c.onerror = function (e) {
reject(e)
}
c.open("GET", url)
headers && headers.forEach(v => {
c.setRequestHeader(v.name, v.value)
})
c.send()
})
}
function httpPost(options) {
let o = Object.assign({
url: '',
responseType: 'json',
type: 'form',
body: null,
timeout: 30000,
headers: [],
}, options)
return new Promise((resolve, reject) => {
let c = new XMLHttpRequest()
c.responseType = o.responseType
c.timeout = o.timeout
c.onload = function (e) {
if (this.status === 200 && this.response !== null) {
resolve(this.response)
} else {
reject(e)
}
}
c.ontimeout = function (e) {
reject(e)
}
c.onerror = function (e) {
reject(e)
}
c.open("POST", o.url)
if (o.type === 'form') {
c.setRequestHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
} else if (o.type === 'json') {
c.setRequestHeader("Content-Type", "application/json; charset=UTF-8")
} else if (o.type === 'xml') {
c.setRequestHeader("Content-Type", "application/ssml+xml")
}
o.headers.length > 0 && o.headers.forEach(v => {
c.setRequestHeader(v.name, v.value)
})
c.send(o.body)
})
}
// 时间范围内,只执行最后一次回调函数
function _setTimeout(tid, callback, timeout) {
tid = `mx_timeoutId_${tid}`
_clearTimeout(tid)
return window[tid] = setTimeout(callback, timeout)
}
function _clearTimeout(tid) {
let id = window[tid]
if (id) {
clearTimeout(id)
window[tid] = null
}
}
function encodeURI(s) {
s = encodeURIComponent(s)
s = s.replace(/#/g, '%23')
s = s.replace(/&/g, '%26')
return s
}
function debug(...data) {
isDebug && console.log('[DMX DEBUG]', ...data)
}
================================================
FILE: src/js/content.js
================================================
'use strict'
/**
* Dream Translate
* https://github.com/ryanker/dream_translate
* @Author Ryan
* @license MIT License
*/
let isPopup = window.isPopup
let isFullscreen, isClipboardRead, isSome
let dialog, shadow,
setting, conf, dialogConf,
languageList, dialogCSS = '', dictionaryCSS = {},
iconBut, iconText,
msgList = {},
root = B.root
let dQuery = {action: '', text: '', source: '', target: ''}
let textTmp = ''
let history = [], historyIndex = 0, disHistory = false
let searchText
document.addEventListener('DOMContentLoaded', async function () {
let u = new URL(location.href)
isFullscreen = u.searchParams.get('fullscreen') === '1'
isClipboardRead = u.searchParams.get('clipboardRead') === '1'
isSome = location.href.indexOf(root) === 0
await storageLocalGet(['conf', 'languageList', 'dialogCSS', 'dictionaryCSS']).then(function (r) {
conf = r.conf
languageList = JSON.parse(r.languageList)
dialogCSS = r.dialogCSS
dictionaryCSS = r.dictionaryCSS
})
await storageSyncGet(['setting', 'dialogConf', 'searchText']).then(function (r) {
setting = r.setting
dialogConf = Object.assign({}, conf.dialogConf, r.dialogConf)
searchText = r.searchText
})
// 初始对话框
initDialog()
// 初始对话框CSS
initDictionaryCSS()
// 是否开启自动解除选中现在
if (setting.allowSelect === 'on' && !isSome) allowUserSelect()
// 查看全部数据
storageShowAll()
})
// 监听消息
B.onMessage.addListener(function (m, sender, sendResponse) {
sendResponse()
debug('request:', m)
// debug('sender:', sender)
if (m.action === 'translate') {
msgList[m.name] = m.result
resultTranslate(m.name)
} else if (m.action === 'dictionary') {
resultDictionary(m)
} else if (m.action === 'playSound') {
resultSound(m)
} else if (m.action === 'link') {
resultLink(m)
} else if (m.action === 'allowSelect') {
allowUserSelect()
} else if (m.action === 'onCrop') {
initCrop()
} else if (m.action === 'onAlert') {
dmxAlert(m.message, m.type)
} else if (m.action === 'contextMenus') {
sendQuery(m.text) // 右键查询
showDialog()
}
})
// 监听 frame 消息
window.addEventListener("message", function (m) {
let d = m.data
if (d.text && typeof d.clientX === 'number' && typeof d.clientY === 'number') initQuery(d.text, d.clientX, d.clientY)
})
// 监听设置修改
B.storage.onChanged.addListener(function (data) {
let keys = Object.keys(data)
keys.forEach(k => {
let v = data[k].newValue
if (k === 'setting') {
setting = v
debug('new setting:', v)
// 初始对话框CSS
initDictionaryCSS()
} else if (k === 'searchText') {
searchText = v
debug('new searchText:', v)
}
})
})
// 初始对话框
function initDialog() {
let isChange = false
let options = {
cssText: dialogCSS,
width: dialogConf.width,
height: dialogConf.height,
minWidth: 450,
onResize: function (style) {
const {width, height} = style
if (width) dialogConf.width = width
if (height) dialogConf.height = height
isChange = true
}
}
if (isPopup) {
options.width = 'auto'
options.height = 'auto'
options.show = true
options.autoHide = false
options.isMove = false
options.isResize = false
options.onResize = null
}
dialog = dmxDialog(options)
// 保存窗口大小
dialog.el.addEventListener('mouseup', function () {
if (isChange) {
saveDialogConf()
isChange = false
}
})
// 影子元素
shadow = dialog.shadow
// 小屏窗口
if (isPopup) {
addClass(dialog.el, 'dmx_popup')
isFullscreen ? addClass(dialog.el, 'fullscreen') : addClass(document.documentElement, 'dmx_popup')
A('#dmx_close,#dmx_pin,#dmx_fullscreen').forEach(e => e.remove())
if (B.getBackgroundPage) textTmp = B.getBackgroundPage().textTmp // 读取后台缓存
}
// 划词查询
document.addEventListener('mouseup', function (e) {
let text = window.getSelection().toString().trim()
initQuery(text, e.clientX, e.clientY)
})
// 鼠标图标
iconBut = I('dmx_mouse_icon')
iconBut.onclick = function (e) {
iconBut.style.display = 'none'
sendQuery(iconText) // 点击图标查询
showDialog(e.clientX + 10, e.clientY - 35)
}
iconBut.onmousedown = function (e) {
e.preventDefault()
}
// 绑定事件
let nav = I('dmx_navigate')
let uEl = nav.querySelectorAll('u')
uEl.forEach(e => {
e.addEventListener('click', function () {
let action = this.getAttribute('action')
if (!['translate', 'dictionary', 'search'].includes(action)) return
if (dQuery.action === action) return
rmClassD(uEl, 'active')
addClass(this, 'active')
setDialogConf('action', action) // 保存设置
if (action === 'translate') {
initTranslate()
} else if (action === 'dictionary') {
initDictionary()
} else if (action === 'search') {
initSearch()
}
sendQuery(dQuery.text) // 切换导航查询
})
})
// 初始模块
let action = dialogConf.action
if (action) {
if (!['translate', 'dictionary', 'search'].includes(action)) action = 'translate'
let actionEl = nav.querySelector(`u[action="${action}"]`)
if (actionEl) actionEl.click()
}
// 设置按钮
I('dmx_setting').addEventListener('click', function () {
rmClassD(uEl, 'active')
initSetting()
dQuery.action = 'setting'
})
// 更多功能
I('dmx_more').addEventListener('click', function () {
rmClassD(uEl, 'active')
initMore()
dQuery.action = 'more'
})
// 录音练习
I('dmx_voice').addEventListener('click', function () {
sendMessage({action: 'onRecord'})
})
// 鼠标停留取词
document.addEventListener('mousemove', (e) => {
if (setting.autoWords && setting.scribble !== 'off') _setTimeout('_mouseWords', () => mouseWords(e), 300)
})
// 历史记录
let hEl = I('dmx_history')
let hlEl = hEl.querySelector('.dmx-icon-left')
let hrEl = hEl.querySelector('.dmx-icon-right')
let loadHistory = function (index) {
if (index < 0 || index >= history.length) return
disHistory = true
historyIndex = index
let className = 'disabled'
rmClass(hlEl, className)
rmClass(hrEl, className)
if (index === 0) {
addClass(hlEl, className)
} else if (index === history.length - 1) {
addClass(hrEl, className)
}
let data = history[index]
debug('current:', historyIndex, data, history)
let action = data.action
let text = data.text
dialogConf.action = action
dialogConf.source = data.source
dialogConf.target = data.target
dQuery.action !== action && setDialogConf('action', action) // 保存设置
rmClassD(uEl, 'active')
addClass(nav.querySelector(`u[action="${action}"]`), 'active')
if (action === 'translate') {
initTranslate()
} else if (action === 'dictionary') {
initDictionary()
} else if (action === 'search') {
initSearch()
}
sendQuery(text) // 历史记录查询
}
hlEl.addEventListener('click', () => loadHistory(historyIndex - 1))
hrEl.addEventListener('click', () => loadHistory(historyIndex + 1))
}
function initTranslate() {
let l = languageList, langList = ''
for (let k in l) {
if (l.hasOwnProperty(k)) langList += `${l[k].zhName}`
}
dialog.contentHTML(`
翻 译
${langList}
`)
// 绑定事件
let sourceEl = I('language_source')
let targetEl = I('language_target')
let exchangeEl = I('language_exchange')
let inputEl = I('translate_input')
let translateEl = I('translate_button')
let cropEl = I('translate_crop')
let dropdownEl = I('language_dropdown')
let dropdownU = dropdownEl.querySelectorAll('u')
let contentEl = I('dmx_dialog_content')
let tmpEl
let onButton = function () {
let el = this
if (tmpEl === el && dropdownEl.style.display === 'block') {
dropdownEl.style.display = 'none'
rmClass(el, 'active')
} else {
tmpEl = el
addClass(el, 'active')
let sourceVal = sourceEl.getAttribute('value')
let targetVal = targetEl.getAttribute('value')
let isSource = el === sourceEl
if (isSource) {
rmClass(targetEl, 'active')
rmClass(dropdownEl, 'dropdown_target')
} else {
rmClass(sourceEl, 'active')
addClass(dropdownEl, 'dropdown_target')
}
dropdownEl.style.display = 'block'
dropdownU.forEach(e => {
rmClass(e, 'active')
rmClass(e, 'disabled')
let val = e.getAttribute('value')
if (isSource) {
if (sourceVal === val) {
addClass(e, 'active')
} else if (targetVal === val) {
addClass(e, 'disabled')
}
} else {
if (targetVal === val) {
addClass(e, 'active')
} else if (sourceVal === val) {
addClass(e, 'disabled')
}
}
})
}
}
sourceEl.addEventListener('click', onButton)
targetEl.addEventListener('click', onButton)
exchangeEl.addEventListener('click', function () {
let sourceVal = sourceEl.getAttribute('value')
let sourceText = sourceEl.innerText
let targetVal = targetEl.getAttribute('value')
let targetText = targetEl.innerText
if (sourceVal === 'auto') return
sourceEl.setAttribute('value', targetVal)
sourceEl.innerText = targetText
targetEl.setAttribute('value', sourceVal)
targetEl.innerText = sourceText
setDialogConf('source', targetVal)
setDialogConf('target', sourceVal)
rmClass(sourceEl, 'active')
rmClass(targetEl, 'active')
dropdownEl.style.display = 'none'
})
translateEl.addEventListener('click', function () {
rmClass(sourceEl, 'active')
rmClass(targetEl, 'active')
dropdownEl.style.display = 'none'
let text = inputEl.innerText.trim()
sendQuery(text) // 翻译按钮查询
})
cropEl.addEventListener('click', function () {
sendBgMessage({action: 'onCropImg'}).then(_ => {
location.href.indexOf(root + 'html/popup.html') === 0 && window.close()
})
})
dropdownU.forEach(e => {
e.addEventListener('click', function () {
let v = this.getAttribute('value')
let s = this.innerText
let isSource = !hasClass(dropdownEl, 'dropdown_target')
let el = isSource ? sourceEl : targetEl
rmClass(el, 'active')
el.setAttribute('value', v)
el.innerText = s
dropdownEl.style.display = 'none'
contentEl.scrollTop = 0
if (isSource) {
(v === 'auto' ? addClass : rmClass)(exchangeEl, 'disabled')
setDialogConf('source', v)
} else {
setDialogConf('target', v)
}
})
})
// 粘贴事件
inputEl.addEventListener('paste', function (e) {
e.stopPropagation()
e.preventDefault()
let d = e.clipboardData || window.clipboardData
if (d && d.items.length > 0) {
let f = d.items[0].getAsFile()
if (f && f.type.indexOf('image') === 0) {
// 如果粘贴内容是图片,进行图片文字识别
let fr = new FileReader()
fr.readAsDataURL(f)
fr.onload = function (e) {
let base64 = e.target.result
sendBgMessage({action: 'img2text', base64})
}
} else {
// 如果是文本,则清理一下
this.innerText = d.getData('Text')
}
}
})
inputEl.addEventListener('blur', function () {
textTmp = this.innerText
})
inputEl.addEventListener('keyup', function () {
if (isPopup && setting.autoConfirm) _setTimeout('translate', () => translateEl.click(), 2000) // 定时自动开始翻译
})
isPopup && focusLast(inputEl) // 光标移到结尾
// 隐藏原文框,减少占用空间
if (setting.hideOriginal) {
if (!isPopup && !isFullscreen) { // 排除弹窗
// translateEl.style.display = 'none'
inputEl.style.display = 'none'
E('.dmx_main_trans').style.paddingTop = '0'
}
}
// 隐藏截图框(在独立窗口的时候)
if (isFullscreen) cropEl.style.display = 'none'
// 初始值
let source = dialogConf.source
let target = dialogConf.target
if (source === 'auto') addClass(exchangeEl, 'disabled')
sourceEl.setAttribute('value', source)
sourceEl.innerText = l[source].zhName
targetEl.setAttribute('value', target)
targetEl.innerText = l[target].zhName
}
function initDictionary() {
dialog.contentHTML(`
`)
let inpEl = I('dictionary_input')
let rmEl = I('search_remove')
let butEl = I('search_but')
rmEl.onclick = function () {
inpEl.value = ''
inpEl.focus()
}
butEl.onclick = function () {
let text = inpEl.value.trim()
sendQuery(text) // 词典按钮查询
}
inpEl.addEventListener('change', function () {
textTmp = this.value
})
inpEl.addEventListener('keyup', function (e) {
e.key === 'Enter' && butEl.click()
if (isPopup) _setTimeout('dictionary', () => butEl.click(), 1000) // 定时自动开始查词
})
setTimeout(() => inpEl.focus(), 100)
}
function initSearch() {
dialog.contentHTML(`
`)
let inpEl = I('search_input')
let rmEl = I('search_remove')
let butEl = I('search_but')
rmEl.onclick = function () {
inpEl.value = ''
inpEl.focus()
}
butEl.onclick = function () {
let el = I('case_list').querySelector('[data-search]')
if (el) el.click()
}
inpEl.addEventListener('change', function () {
textTmp = this.value
})
inpEl.addEventListener('keyup', function (e) {
e.key === 'Enter' && butEl.click()
})
setTimeout(() => inpEl.focus(), 100)
// 创建按钮
let s = ''
let sList = setting.searchList
let cList = getSearchList(searchText)
for (let name of sList) {
if (cList[name]) s += `
${name}
`
}
I('case_list').innerHTML = s
// 绑定点击事件
onD(A('[data-search]'), 'click', function () {
let name = this.dataset.search
let url = cList[name]
if (!url) return
let text = I('search_input').value.trim()
if (text) {
open(url.format(decodeURIComponent(text)))
} else {
open((new URL(url)).origin)
}
})
}
function initSetting() {
dialog.contentHTML(``)
}
function initMore() {
dialog.contentHTML(``)
}
function initDictionaryCSS() {
let styleEl = E('style')
conf.dictionaryCSS.forEach(name => {
if (!setting.dictionaryList.includes(name) || !dictionaryCSS[name] || E(`style[data-name="${name}"]`)) return
let s = ``
styleEl.insertAdjacentHTML('afterend', s)
})
}
function initCrop() {
let startX = 0, startY = 0
let bgEl = I('dmx_crop_bg')
let fgEl = I('dmx_crop_fg')
bgEl.style.display = 'block'
let funDown = (e) => {
startX = e.clientX
startY = e.clientY
fgEl.style.display = 'block'
fgEl.style.left = startX + 'px'
fgEl.style.top = startY + 'px'
document.addEventListener('mousemove', funMove)
document.addEventListener('mouseup', funUp)
}
let funMove = (e) => {
let w = e.clientX - startX
let h = e.clientY - startY
if (w > 0) {
fgEl.style.width = w + 'px'
} else {
fgEl.style.left = e.clientX + 'px'
fgEl.style.width = -w + 'px'
}
if (h > 0) {
fgEl.style.height = h + 'px'
} else {
fgEl.style.top = e.clientY + 'px'
fgEl.style.height = -h + 'px'
}
}
let funUp = (e) => {
bgEl.removeAttribute('style')
fgEl.removeAttribute('style')
bgEl.removeEventListener('mousedown', funDown)
document.removeEventListener('mousemove', funMove)
document.removeEventListener('mouseup', funUp)
let width = e.clientX - startX
let height = e.clientY - startY
if (width < 0) {
width = -width
startX = e.clientX
}
if (height < 0) {
height = -height
startY = e.clientY
}
let innerHeight = window.innerHeight || document.documentElement.offsetHeight
if (width > 15 && height > 15) {
sendBgMessage({action: 'onCapture', startX, startY, width, height, innerHeight})
dmxAlert('截图文字识别中...', 'success')
} else {
dmxAlert('截图太小,取消识别', 'error')
}
}
bgEl.addEventListener('mousedown', funDown)
}
function loadingTranslate() {
let el = I('case_list')
let cList = conf.translateList
let sList = setting.translateList
if (sList.length < 1) {
el.innerHTML = `
您未启用任何翻译模块
`
return
}
let s = ''
sList.forEach(name => {
s += `
`
})
el.innerHTML = s
}
function resultTranslate(name, isBilingual) {
let el = I(`${name}_translate_case`)
if (!el) return
let {srcLan, tarLan, lanTTS, data, extra} = msgList[name] || {}
// 显示发音图标
if (srcLan && tarLan) {
let sourceStr = soundIconHTML(srcLan, lanTTS, 'source')
let targetStr = soundIconHTML(tarLan, lanTTS, 'target')
el.querySelector('.case_language').innerHTML = `${sourceStr} » ${targetStr}`
let sourceEl = el.querySelector('[data-type=source]')
let targetEl = el.querySelector('[data-type=target]')
sourceEl && sourceEl.addEventListener('click', function () {
activeRipple(this)
sendPlayTTS(name, 'source', srcLan, dQuery.text) // 播放原音
})
targetEl && targetEl.addEventListener('click', function () {
activeRipple(this)
let s = ''
data && data.forEach(v => {
s += v.tarText + '\n'
})
s && sendPlayTTS(name, 'target', tarLan, s) // 播放译音
})
}
// 显示翻译结果
let s = ''
data && data.forEach(v => {
if (isBilingual) {
s += `
${v.srcText}
${v.tarText}
`
} else {
s += `
${v.tarText}
`
}
})
if (extra) s += extra // 重点词汇 && 单词含义
if (!s) s = '网络错误,请稍后再试'
el.querySelector('.case_content').innerHTML = s
// 绑定点击搜索
resultBindEvent(el, 'translate', name)
}
function resultDictionary(m) {
let {name, result, error} = m
let el = I(`${name}_dictionary_case`)
if (!el) return
let cEl = el.querySelector('.case_content')
if (error) {
cEl.innerHTML = '网络错误,请稍后再试'
return
}
let {html, phonetic, sound} = result || {}
// 音标
let pron = ''
if (phonetic) {
let {uk, us} = phonetic
if (uk && us) {
pron += `[${uk} $ ${us}]`
} else if (uk) {
pron += `[${uk}]`
} else if (us) {
pron += `[$ ${us}]`
}
}
// 发音
sound && sound.forEach(v => {
let {isWoman, type, url, title} = v
let className = isWoman ? 'dmx_pink' : ''
if (!title) title = type === 'uk' ? '英音' : type === 'us' ? '美音' : ''
pron += ` `
})
if (!html) html = 'Sorry! 没有查询到结果。'
el.querySelector('.case_content').innerHTML = html
el.querySelector('.case_pronunciation').innerHTML = pron
resultBindEvent(el, 'dictionary', m.name)
}
function resultBindEvent(el, nav, name) {
// 绑定播放音频
el.querySelectorAll('[data-src-mp3]').forEach(e => {
let obj = {uk: '', us: '', en: '', other: ''}
let type = e.getAttribute('data-type')
if (!obj[type]) {
type = 'other'
e.setAttribute('data-type', type)
}
e.innerHTML = obj[type] // 喇叭字体
e.addEventListener('click', function () {
activeRipple(this)
let type = this.getAttribute('data-type')
let url = this.getAttribute('data-src-mp3')
sendPlaySound(nav, name, type, url)
})
})
// 绑定点击搜索
el.querySelectorAll('[data-search=true]').forEach(e => {
e.addEventListener('click', function () {
let text = this.innerText && this.innerText.trim()
sendQuery(text) // 结果点击查询
})
})
}
function resultLink(m) {
let el = I(`${m.name}_${m.type}_case`)
if (!el) return
let sEl = el.querySelector(`.case_link`)
if (sEl) {
sEl.setAttribute('href', m.link)
sEl.setAttribute('target', '_blank')
sEl.setAttribute('referrerPolicy', 'no-referrer')
}
}
function resultSound(m) {
let {nav, name, type, status, error} = m
// if (error) dmxAlert(error, 'error') // 播放声音出错提示
let el = I(`${name}_${nav}_case`)
if (!el) return
if (status === 'start') {
let sEl = el.querySelector(`[data-type=${type}]`)
if (sEl) addClass(sEl, 'active')
} else {
let dEl = el.querySelectorAll(`[data-type=${type}]`)
if (dEl) rmClassD(dEl, 'active')
}
addClass(I('dmx_voice'), 'dmx_show')
}
function activeRipple(el) {
rmClassD(A('.dmx_ripple'), 'active')
addClass(el, 'active')
}
function soundIconHTML(lan, lanArr, type) {
let title = languageList[lan] ? languageList[lan].zhName : ''
let arr = {
'zh': '',
'en': '',
'jp': '',
'th': '',
'spa': '',
'ara': '',
'fra': '',
'kor': '',
'ru': '',
'de': '',
'pt': '',
'it': '',
'el': '',
'nl': '',
'pl': ''
}
let iconStr = arr[lan] || ''
let s = title
if (!lanArr || inArray(lan, lanArr)) {
s += ` ${iconStr}`
}
return s
}
// 发送到后台缓存起来
function sendBgCache(text) {
if (window.textRepeat !== text) {
window.textRepeat = text
sendBgMessage({action: 'textTmp', text})
}
}
// 自动切换翻译或词典
function autoChangeAction(text) {
if (!setting.autoChange) return
let arr = text.match(/\s+/g)
let isWord = !arr || arr.length < 3 // 是否为单词活词组
A('#dmx_navigate > u').forEach(el => rmClass(el, 'active')) // 去掉选中
let onEl = E(`#dmx_navigate > u[action="${isWord ? 'dictionary' : 'translate'}"]`)
if (onEl) {
// 选中翻译或词典
addClass(onEl, 'active')
onEl.click()
}
}
function initQuery(text, clientX, clientY) {
// Unicode property escapes 正则表达式:
// https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Regular_Expressions/Unicode_Property_Escapes
// https://www.unicode.org/Public/UCD/latest/ucd/PropertyValueAliases.txt
// https://tc39.es/ecma262/#table-nonbinary-unicode-properties
// https://keqingrong.github.io/blog/2020-01-29-regexp-unicode-property-escapes
if (setting.excludeChinese && /\p{Script=Han}/u.test(text)) return // 排除中文
if (setting.excludeSymbol && /^[\p{S}\p{P}^$.*+\-?=!:|\\/!?。;-_~﹏,:、·;…,"“”﹃﹄「」﹁﹂『』﹃﹄()[]〔〕【】《》〈〉()\[\]{}<>\s]+$/u.test(text)) return // 排除纯符号
if (setting.excludeNumber && /^\d+$/u.test(text)) return // 排除纯数字
sendBgCache(text)
if (!text) {
iconBut.style.display = 'none'
return
}
debug('text:', text)
// 自动复制功能
if (setting.autoCopy === 'on') {
sendBgMessage({action: 'copy', text})
dmxAlert('复制成功', 'success')
}
if (setting.scribble === 'off') return
if (setting.scribble === 'direct') {
autoChangeAction(text) // 自动切换翻译或词典
sendQuery(text) // 划词查询
showDialog(clientX + 30, clientY - 60)
} else if (setting.scribble === 'clickIcon') {
iconText = text
let x = clientX + 10
let y = clientY - 45
x = x + 42 < document.documentElement.clientWidth ? x : clientX - 42
y = y > 10 ? y : clientY + 10
iconBut.style.transform = `translate(${x}px, ${y}px)`
iconBut.style.display = 'flex'
}
}
function sendQuery(text) {
if (!text) {
if (isClipboardRead) text = execPaste() // 自动读取粘贴板内容
if (!text && isPopup && setting.autoPaste === 'on') text = execPaste()
if (!text) text = textTmp
if (!text) return
}
let el = I('dmx_navigate')
let action = el.querySelector('.active') && el.querySelector('.active').getAttribute('action')
if (!action) {
action = dialogConf.action
let actionEl = el.querySelector(`u[action="${action}"]`)
if (actionEl) actionEl.click()
}
if (!checkChange(action, text)) return
let message = null
if (setting.cutHumpName === 'on' && action !== 'search') text = cutHumpName(text)
if (action === 'translate') {
let inputEl = I(`translate_input`)
inputEl.innerText = text
isPopup && focusLast(inputEl)
loadingTranslate()
message = {action: action, text: text, srcLan: dialogConf.source, tarLan: dialogConf.target}
} else if (action === 'dictionary') {
I(`dictionary_input`).value = text
loadingDictionary()
message = {action: action, text: text}
} else if (action === 'search') {
I(`search_input`).value = text
}
showSearchSide(text)
if (message) {
message = Object.assign({formTitle: document.title, formUrl: location.href}, message)
sendBgMessage(message)
}
}
// 切分驼峰词组
function cutHumpName(s) {
let isCapital = function (s) {
let c = s.charAt(0)
return c >= 'A' && c <= 'Z'
}
return s.replace(/\w+[A-Z]\w?/g, (...args) => {
let str = args[0]
let newStr = ''
for (let i = 0; i < str.length; i++) {
let l = str[i]
if (l === '_') l = ' '
else if (isCapital(l) && (i - 1 > 0 && str[i - 1] !== '_' && !isCapital(str[i - 1]))) newStr += ' '
newStr += l
}
return newStr
})
}
function mouseWords(e) {
let x = e.clientX
let y = e.clientY
if (!x || !y) return
let textNode, offset, arr
if (document.caretPositionFromPoint) {
let p = document.caretPositionFromPoint(x, y)
if (!p) return
textNode = p.offsetNode
offset = p.offset
} else if (document.caretRangeFromPoint) {
let p = document.caretRangeFromPoint(x, y)
if (!p) return
textNode = p.startContainer
offset = p.startOffset
}
if (!textNode || textNode.nodeType !== 3) return
let str = textNode.data
let before = (arr = str.slice(0, offset).match(/[a-z’']+$/i)) ? arr[0] : ''
let after = (arr = str.slice(offset).match(/^[a-z’']+/i)) ? arr[0] : ''
if (before.length === 0 && after.length === 0) return
let range = document.createRange()
range.setStart(textNode, offset - before.length)
range.setEnd(textNode, offset + after.length)
let bcr = range.getBoundingClientRect()
if (x >= bcr.left && x <= bcr.right && y >= bcr.top && y <= bcr.bottom) {
let se = window.getSelection()
se.removeAllRanges()
se.addRange(range)
let text = before + after
if (text && window.textRepeat !== text) {
sendBgCache(text)
sendQuery(text) // 鼠标停留获取单词
showDialog()
}
}
range.detach()
}
function showSearchSide(text) {
let arr = setting.searchSide
let s = ''
if (text && isArray(arr) && arr.length > 0) {
let sList = getSearchList(searchText)
for (let name of arr) {
let url = sList[name]
if (url) s += `${name[0]}`
}
}
I('dmx_dialog_left').innerHTML = s
}
function showDialog(left, top) {
let options = null
let position = setting.position
if (isPopup) {
// 跳过
} else if (position === 'follow') {
options = {left, top}
} else if (position === 'right') {
dialog.el.removeAttribute('style')
dialog.el.style.width = dialogConf.width + 'px'
dialog.el.className = 'dmx_keep_right'
}
dialog.show(options)
}
function checkChange(action, text) {
let d = dQuery
let source = dialogConf.source
let target = dialogConf.target
if (d.action === action && d.text === text && d.source === source && d.target === target) return false
dQuery = {action, text, source, target}
addHistory(dQuery)
return true
}
function addHistory(dQuery) {
if (disHistory) return disHistory = false
if (!['translate', 'dictionary', 'search'].includes(dQuery.action)) return
if (historyIndex < history.length - 1) {
history.splice(historyIndex + 1, history.length)
} else if (history.length >= 1000) {
history.shift() // 最多只保留 1000 条
}
history.push(dQuery)
historyIndex = history.length - 1
debug('history:', history, historyIndex)
if (history.length > 1) {
let hEl = I('dmx_history')
rmClass(hEl.querySelector('.dmx-icon-left'), 'disabled')
addClass(hEl.querySelector('.dmx-icon-right'), 'disabled')
}
}
function focusLast(el) {
setTimeout(() => {
el.focus()
let range = window.getSelection()
range.selectAllChildren(el)
range.collapseToEnd() // 光标移到结尾
}, 200)
}
function I(id) {
return shadow.getElementById(id)
}
function E(s) {
return shadow.querySelector(s)
}
function A(s) {
return shadow.querySelectorAll(s)
}
function saveDialogConf() {
storageSyncSet({'dialogConf': dialogConf})
}
function setDialogConf(name, value) {
dialogConf[name] = value
saveDialogConf()
}
function allowUserSelect() {
dmxAlert('解除页面限制完成', 'success')
let styleEl = document.getElementById('_dream_style_all')
if (!styleEl) {
let sEl = document.createElement('style')
sEl.id = '_dream_style_all'
sEl.textContent = `* {-webkit-user-select:text!important;-moz-user-select:text!important;user-select:text!important;pointer-events:auto!important;}`
document.head.appendChild(sEl)
}
let onAllow = function (el, event) {
if (el.getAttribute && el.getAttribute(event)) el.setAttribute(event, () => true)
}
onAllow(document, 'oncontextmenu')
onAllow(document, 'onselectstart')
// 清除再添加,排除重复事件
let onClean = function (e) {
e.stopPropagation()
let el = e.target
while (el) {
onAllow(el, 'on' + e.type)
el = el.parentNode
}
}
document.removeEventListener('contextmenu', onClean, true)
document.removeEventListener('selectstart', onClean, true)
document.addEventListener('contextmenu', onClean, true)
document.addEventListener('selectstart', onClean, true)
}
function sendPlayTTS(name, type, lang, text) {
sendBgMessage({action: 'translateTTS', name, type, lang, text})
}
function sendPlaySound(nav, name, type, url) {
sendBgMessage({action: 'playSound', nav, name, type, url})
}
function sendBgMessage(message) {
return new Promise((resolve, reject) => {
sendMessage(message).then(_ => {
resolve()
}).catch(err => {
// 减少错误提示框
if (getTimestamp() > (window.dmxUpdateDate || 0)) {
window.dmxUpdateDate = getTimestamp() + 5
dmxAlert('梦想翻译已升级,请刷新页面激活。', 'error')
}
debug('sendBgMessage error:', err)
// reject(err)
resolve()
})
})
}
function dmxAlert(message, type, timeout) {
type = type || 'info'
timeout = timeout || 2500
let el = I('dmx_alert')
if (!el) {
el = document.createElement('div')
el.id = 'dmx_alert'
shadow.appendChild(el)
}
let icon = {
info: '',
error: '',
success: '',
}
let m = document.createElement('div')
m.className = `dxm_alert_${type}`
m.innerHTML = (icon[type] || '') + message
el.appendChild(m)
setTimeout(() => {
addClass(m, 'an_top')
}, 10)
setTimeout(() => {
addClass(m, 'an_delete')
setTimeout(() => {
el.removeChild(m)
}, 300)
}, timeout)
}
function dmxDialog(options) {
if (window._MxDialog) return window._MxDialog
let o = Object.assign({
width: 500,
height: 300,
minWidth: 200,
minHeight: 200,
show: false,
autoHide: true,
isMove: true,
isResize: true,
onResize: null,
cssText: '',
contentHTML: '',
}, options || {})
let d = document.createElement('div')
d.setAttribute('mx-name', 'dream-translation')
document.documentElement.appendChild(d)
shadow = d.attachShadow({mode: 'closed'})
shadow.innerHTML = `
翻译词典搜索
${o.contentHTML}
`
let el = I('dmx_dialog')
let clientX, clientY, elX, elY, elW, elH, docW, docH, mid
let _m = function (e) {
let left = e.clientX - (clientX - elX)
let top = e.clientY - (clientY - elY)
let maxLeft = docW - elW
let maxTop = docH - elH
left = Math.max(0, Math.min(left, maxLeft))
top = Math.max(0, Math.min(top, maxTop))
el.style.left = left + 'px'
el.style.top = top + 'px'
}
let _n = function (e) {
let top = e.clientY - (clientY - elY)
let height = elY - top + elH
if (height > o.minHeight && top >= 0) {
el.style.top = top + 'px'
el.style.height = height + 'px'
typeof o.onResize === 'function' && o.onResize({height: height})
}
}
let _e = function (e) {
let left = e.clientX - (clientX - elX)
let width = left - elX + elW
if (width > o.minWidth && e.clientX < docW - (elW - (clientX - elX))) {
el.style.width = width + 'px'
typeof o.onResize === 'function' && o.onResize({width: width})
}
}
let _s = function (e) {
let top = e.clientY - (clientY - elY)
let height = top - elY + elH
if (e.clientY < docH - (elH - (clientY - elY)) && height > o.minHeight) {
el.style.height = height + 'px'
typeof o.onResize === 'function' && o.onResize({height: height})
}
}
let _w = function (e) {
let left = e.clientX - (clientX - elX)
let width = elW - (left - elX)
if (left >= 0 && width > o.minWidth) {
el.style.left = left + 'px'
el.style.width = width + 'px'
typeof o.onResize === 'function' && o.onResize({width: width})
}
}
let onMousedown = function (e) {
e.stopPropagation()
mid = this.id
clientX = e.clientX
clientY = e.clientY
let b = el.getBoundingClientRect()
elX = b.left || el.offsetLeft
elY = b.top || el.offsetTop
elW = b.width || el.offsetWidth
elH = b.height || el.offsetHeight
docW = document.documentElement.clientWidth
docH = document.documentElement.clientHeight
addClass(document.body, 'dmx_unselectable')
addClass(el, 'dmx_unselectable')
}
let onMouseup = function (e) {
// e.stopPropagation() // 和B站播放进度条冲突
mid = null
rmClass(document.body, 'dmx_unselectable')
rmClass(el, 'dmx_unselectable')
}
let onMousemove = function (e) {
if (mid === 'dmx_dialog_title') {
_m(e)
} else if (mid === 'dmx_dialog_resize_n') {
_n(e)
} else if (mid === 'dmx_dialog_resize_e') {
_e(e)
} else if (mid === 'dmx_dialog_resize_s') {
_s(e)
} else if (mid === 'dmx_dialog_resize_w') {
_w(e)
} else if (mid === 'dmx_dialog_resize_nw') {
_n(e)
_w(e)
} else if (mid === 'dmx_dialog_resize_ne') {
_n(e)
_e(e)
} else if (mid === 'dmx_dialog_resize_sw') {
_s(e)
_w(e)
} else if (mid === 'dmx_dialog_resize_se') {
_s(e)
_e(e)
}
}
document.addEventListener('mousemove', onMousemove)
document.addEventListener('mouseup', onMouseup)
document.addEventListener('mouseleave', onMouseup) // 鼠标离开浏览器
el.addEventListener('mouseup', function (e) {
e.stopPropagation() // 解决点击面板闪耀问题
onMouseup()
})
let elArr = ['n', 'e', 's', 'w', 'nw', 'ne', 'sw', 'se']
let fsTmp = {} // 全屏设置临时缓存
let D = {}
D.el = el
D.shadow = shadow
D.destroy = function () {
document.removeEventListener('mousemove', onMousemove)
document.removeEventListener('mouseup', onMouseup)
document.removeEventListener('mouseup', D.hide)
el.remove()
}
D.show = function (o) {
setTimeout(() => {
el.style.display = 'block'
if (!o || typeof o.left !== 'number' || typeof o.top !== 'number') return
let d = document.documentElement
let b = el.getBoundingClientRect()
el.style.left = Math.max(0, Math.min(o.left, d.clientWidth - b.width)) + 'px'
el.style.top = Math.max(0, Math.min(o.top, d.clientHeight - b.height)) + 'px'
}, 80)
}
D.hide = function () {
el.style.display = 'none'
}
D.enableMove = function () {
let e = I('dmx_dialog_title')
e.style.cursor = 'move'
e.addEventListener('mousedown', onMousedown)
}
D.disableMove = function () {
let e = I('dmx_dialog_title')
e.style.cursor = 'auto'
e.removeEventListener('mousedown', onMousedown)
}
D.enableResize = function () {
elArr.forEach(v => {
let e = I(`dmx_dialog_resize_${v}`)
e.removeAttribute('style')
e.addEventListener('mousedown', onMousedown)
})
}
D.disableResize = function () {
elArr.forEach(v => {
let e = I(`dmx_dialog_resize_${v}`)
e.style.display = 'none'
e.removeEventListener('mousedown', onMousedown)
})
}
D.fullScreen = function () {
addClass(I('dmx_fullscreen'), 'active')
addClass(document.body, 'dmx_overflow_hidden')
fsTmp = {top: el.style.top, left: el.style.left, width: el.style.width, height: el.style.height}
el.style.top = '0'
el.style.left = '0'
el.style.width = document.documentElement.clientWidth + 'px'
el.style.height = document.documentElement.clientHeight + 'px'
}
D.fullScreenExit = function () {
rmClass(I('dmx_fullscreen'), 'active')
rmClass(document.body, 'dmx_overflow_hidden')
if (typeof fsTmp.top === 'string') el.style.top = fsTmp.top
if (typeof fsTmp.left === 'string') el.style.left = fsTmp.left
if (typeof fsTmp.width === 'string') el.style.width = fsTmp.width
if (typeof fsTmp.height === 'string') el.style.height = fsTmp.height
}
D.pin = function () {
addClass(I('dmx_pin'), 'active')
document.removeEventListener('mouseup', D.hide)
}
D.pinCancel = function () {
rmClass(I('dmx_pin'), 'active')
document.addEventListener('mouseup', D.hide) // 点击 body 隐藏 dialog
}
D.contentHTML = function (s) {
I('dmx_dialog_content').innerHTML = s
}
window._MxDialog = D
// 初始设置
if (o.width !== 'auto') el.style.width = Number(o.width) + 'px'
if (o.height !== 'auto') el.style.height = Number(o.height) + 'px'
o.show ? D.show() : D.hide()
o.autoHide ? D.pinCancel() : D.pin()
o.isMove ? D.enableMove() : D.disableMove()
o.isResize ? D.enableResize() : D.disableResize()
// 顶部按钮事件
I('dmx_close').onclick = function () {
D.hide()
rmClass(document.body, 'dmx_overflow_hidden')
}
I('dmx_pin').onclick = function () {
hasClass(this, 'active') ? D.pinCancel() : D.pin()
}
I('dmx_fullscreen').onclick = function () {
hasClass(this, 'active') ? D.fullScreenExit() : D.fullScreen()
}
// 阻止冒泡
shadow.querySelectorAll('.dmx-icon').forEach(v => {
v.addEventListener('mousedown', e => e.stopPropagation())
})
return D
}
================================================
FILE: src/js/db.js
================================================
'use strict'
/**
* Dream Translate
* https://github.com/ryanker/dream_translate
* @Author Ryan
* @license MIT License
*/
function idb(dbName, version, onupgradeneeded) {
return new Promise((resolve, reject) => {
let req = window.indexedDB.open(dbName, version)
req.onupgradeneeded = onupgradeneeded // 首次创建或更高版本号时执行
req.onerror = (e) => reject(e)
req.onsuccess = () => {
let db = req.result
resolve({
db,
rStore(storeName) {
return db.transaction([storeName], 'readonly').objectStore(storeName)
},
wStore(storeName) {
return db.transaction([storeName], 'readwrite').objectStore(storeName)
},
read(storeName, id) {
return new Promise((resolve, reject) => {
let row = this.rStore(storeName).get(id)
row.onsuccess = () => resolve(row.result)
row.onerror = (e) => reject(e)
})
},
readByIndex(storeName, indexName, key) {
return new Promise((resolve, reject) => {
let row = this.rStore(storeName).index(indexName).get(key)
row.onsuccess = () => resolve(row.result)
row.onerror = (e) => reject(e)
})
},
create(storeName, data) {
return new Promise((resolve, reject) => {
let row = this.wStore(storeName).add(data)
row.onsuccess = (e) => resolve(e)
row.onerror = (e) => reject(e)
})
},
update(storeName, id, data) {
return new Promise((resolve, reject) => {
let wStore = this.wStore(storeName)
let row = wStore.get(id)
row.onsuccess = () => {
if (!row.result) return reject('result empty!')
let newData = Object.assign(row.result, data) // 覆盖
let r = wStore.put(newData)
r.onsuccess = (e) => resolve(e)
r.onerror = (e) => reject(e)
}
row.onerror = (e) => reject(e)
})
},
delete(storeName, id) {
return new Promise((resolve, reject) => {
let r = this.wStore(storeName).delete(id)
r.onsuccess = (e) => resolve(e)
r.onerror = (e) => reject(e)
})
},
clear(storeName) {
return new Promise((resolve, reject) => {
let r = this.wStore(storeName).clear()
r.onsuccess = (e) => resolve(e)
r.onerror = (e) => reject(e)
})
},
count(storeName, indexName, query) {
return new Promise((resolve, reject) => {
let store = this.rStore(storeName)
let r = indexName ? store.index(indexName).count(query) : store.count(query)
r.onsuccess = () => resolve(r.result)
r.onerror = (e) => reject(e)
})
},
getAll(storeName, indexName, query, count) {
return new Promise((resolve, reject) => {
let store = this.rStore(storeName)
let req = indexName ? store.index(indexName).getAll(query, count) : store.getAll(query, count)
req.onsuccess = () => resolve(req.result)
req.onerror = (e) => reject(e)
})
},
find(storeName, option) {
let {indexName, query, direction, offset, limit} = option || {}
return new Promise((resolve, reject) => {
let arr = []
let store = this.rStore(storeName)
let req = indexName ? store.index(indexName).openCursor(query, direction) : store.openCursor(query, direction)
let isAdvance = false
req.onsuccess = (e) => {
// let row = e.target.result
let row = req.result
if (row) {
// 偏移量
if (offset && !isAdvance) {
row.advance(offset)
isAdvance = true
return
}
arr.push(row.value) // 返回值
if (limit && arr.length >= limit) {
resolve(arr)
} else {
row.continue()
}
} else {
resolve(arr)
}
}
req.onerror = (e) => reject(e)
})
},
})
}
})
}
function rmIdb(dbName) {
return new Promise((resolve, reject) => {
let db = window.indexedDB.deleteDatabase(dbName)
db.onsuccess = (e) => resolve(e)
db.onerror = (e) => reject(e)
setTimeout(_ => reject('time out'), 2000)
})
}
// 创建存储对象: 收藏
function initFavorite(e) {
let store, db = e.target.result
// sentence
store = db.createObjectStore('sentence', {keyPath: 'id', autoIncrement: true})
store.createIndex('id', 'id', {unique: true})
store.createIndex('cateId', 'cateId') // 分类ID
store.createIndex('sentence', 'sentence', {unique: true}) // 句子
store.createIndex('words', 'words') // 生词,一行一个
store.createIndex('remark', 'remark') // 备注
store.createIndex('records', 'records') // 练习次数
store.createIndex('days', 'days') // 练习天数
store.createIndex('url', 'url') // 音频 URL
store.createIndex('blob', 'blob') // 音频二进制文件
store.createIndex('practiceDate', 'practiceDate') // 最后练习时间
store.createIndex('createDate', 'createDate') // 创建时间
// cate
store = db.createObjectStore('cate', {keyPath: 'cateId', autoIncrement: true})
store.createIndex('cateId', 'cateId', {unique: true}) // 分类ID
store.createIndex('cateName', 'cateName', {unique: true}) // 分类名称
store.createIndex('updateDate', 'updateDate') // 更新时间
store.createIndex('createDate', 'createDate') // 创建时间
// cate 初始分类
setTimeout(() => {
let d = new Date().toJSON()
let row = {cateId: 0, cateName: '最新收藏', updateDate: d, createDate: d}
db.transaction(['cate'], 'readwrite').objectStore('cate').add(row)
}, 100)
}
// 创建存储对象: 历史
function initHistory(e) {
let store, db = e.target.result
// history
store = db.createObjectStore('history', {keyPath: 'id', autoIncrement: true})
store.createIndex('id', 'id', {unique: true})
store.createIndex('content', 'content')
store.createIndex('formTitle', 'formTitle')
store.createIndex('formUrl', 'formUrl')
store.createIndex('createDate', 'createDate') // 创建时间
}
================================================
FILE: src/js/dictionary/bing.js
================================================
'use strict'
/**
* Dream Translate
* https://github.com/ryanker/dream_translate
* @Author Ryan
* @license MIT License
*/
function bingDictionary() {
return {
init() {
return this
},
unify(r, q) {
let s = ''
let phonetic = {} // 音标
let sound = [] // 发音
let el = r.querySelector('.lf_area')
// 查询单词
let wordEl = el.querySelector('#headword')
if (wordEl) s = `
${wordEl.innerText.trim()}
`
// 音标
let prUK = el.querySelector('.hd_pr')
if (prUK) {
let ph = prUK.innerText ? prUK.innerText.replace(/^UK|[\[\]美英]/g, '').trim() : ''
if (ph) phonetic.uk = ph
}
let prUS = el.querySelector('.hd_prUS')
if (prUS) {
let ph = prUS.innerText ? prUS.innerText.replace(/^US|[\[\]美英]/g, '').trim() : ''
if (ph) phonetic.us = ph
}
if (phonetic.us && phonetic.uk === phonetic.us) delete phonetic.us // 如果音标一样,只保留一个
// 发音
let tfEl = el.querySelectorAll('.hd_tf')
if (tfEl && tfEl.length >= 2) {
let getSoundUrl = function (e) {
let url = ''
let aEl = e.querySelector('a')
if (!aEl) return url
let str = aEl.getAttribute('onclick')
str && str.replace(/'(http[^']+)'/, r => url = r.replace(/'/g, ''))
return url
}
let ukUrl = getSoundUrl(tfEl[1])
if (ukUrl) sound.push({type: 'uk', url: ukUrl})
let usUrl = getSoundUrl(tfEl[0])
if (usUrl) sound.push({type: 'us', url: usUrl})
}
// 释义
let liEl = el.querySelectorAll('.qdef > ul > li')
if (liEl && liEl.length > 0) {
s += `
`
liEl.forEach(e => {
let bEl = e.querySelector('span.pos')
let tEl = e.querySelector('span.b_regtxt')
let bStr = bEl && bEl.innerText ? `${bEl.innerText.trim()}` : ''
let part = tEl && tEl.innerText ? tEl.innerText.trim() : ''
if (part) s += `
${bStr}${part}
`
})
s += `
`
} else {
let str = ''
el.querySelectorAll('div[class^="p1-"]').forEach(e => {
let tex = e.textContent && e.textContent.trim()
if (tex) str += `
${tex}
`
})
if (str) s += `
${str}
`
}
// 单词形态
let shapeEl = el.querySelector('.hd_div1')
if (shapeEl) {
let shapeStr = ''
shapeEl.querySelectorAll('span,a').forEach(e => {
if (e.tagName === 'SPAN') {
shapeStr += `${e.innerText}`
} else if (e.tagName === 'A') {
shapeStr += `${e.innerText}`
}
})
if (shapeStr) s += `
${shapeStr}
`
}
// 单词图片
let imgEl = el.querySelectorAll('.img_area > .simg > a')
if (imgEl && imgEl.length > 0) {
let imgStr = ''
imgEl.forEach(e => {
let url = e.getAttribute('href')
let iEl = e.querySelector('img')
if (url && iEl) {
let src = iEl.getAttribute('src')
imgStr += ``
}
})
if (imgStr) s += `
${imgStr}
`
}
return {text: q, phonetic, sound, html: s}
},
query(q) {
return new Promise((resolve, reject) => {
// if (q.length > 100) return reject('The text is too large!')
let url = `https://cn.bing.com/dict/search?q=${encodeURIComponent(q)}`
httpGet(url, 'document').then(r => {
if (r) {
resolve(this.unify(r, q))
} else {
reject('bing error!')
}
}).catch(e => {
reject(e)
})
})
},
link(q) {
return `https://cn.bing.com/dict/search?q=${encodeURIComponent(q)}`
},
}
}
================================================
FILE: src/js/dictionary/cambridge.js
================================================
'use strict'
/**
* Dream Translate
* https://github.com/ryanker/dream_translate
* @Author Ryan
* @license MIT License
*/
function cambridgeDictionary() {
return {
enUrl: 'https://dictionary.cambridge.org/dictionary/english/',
zHUrl: 'https://dictionary.cambridge.org/dictionary/english-chinese-simplified/',
init() {
return this
},
unify(r, q) {
let s = ''
let phonetic = {} // 音标
let sound = [] // 发音
let el = r.querySelector('.entry-body')
let posHeadEl = el.querySelector('.pos-header')
if (posHeadEl) {
// 查询单词
let wordEl = posHeadEl.querySelector('.di-title')
if (wordEl) s = `
${wordEl.innerText}
`
posHeadEl.querySelectorAll('.dpron-i').forEach(e => {
let pEl = e.querySelector('.ipa')
let mEl = e.querySelector('source[type="audio/mpeg"]')
let ph = pEl && pEl.innerText && pEl.innerText.trim()
let src = mEl && mEl.getAttribute('src') || ''
let pre = 'https://dictionary.cambridge.org/'
let type = ''
if (e.className.includes('uk')) {
type = 'uk'
if (ph) phonetic.uk = ph
} else if (e.className.includes('us')) {
type = 'us'
if (ph) phonetic.us = ph
} else {
type = 'en'
if (ph) phonetic.uk = ph
}
if (src) sound.push({type, url: pre + src})
})
if (phonetic.us && phonetic.uk === phonetic.us) delete phonetic.us // 如果音标一样,只保留一个
}
// 释义
let part = ''
let posEl = el.querySelector('.pos-header .posgram')
if (posEl) part += `
${posEl.innerText}
`
let transEl = el.querySelectorAll('.pos-body .dsense')
if (transEl && transEl.length > 0) {
transEl.forEach(tEl => {
cleanAttr(tEl, ['title', 'class', 'href'])
el.querySelectorAll('a[href]').forEach(e => {
if (e.href.includes('dictionary/english-chinese-simplified/')) e.setAttribute('data-search', 'true')
e.removeAttribute('href')
})
part += tEl.innerHTML
})
}
if (part) s += `
${part}
`
return {text: q, phonetic, sound, html: s}
},
query(q) {
return new Promise((resolve, reject) => {
if (q.length > 100) return reject('The text is too large!')
let url = this.zHUrl + encodeURIComponent(q)
httpGet(url, 'document').then(r => {
if (r) {
resolve(this.unify(r, q))
} else {
reject('dictionary.cambridge.org error!')
}
}).catch(e => {
reject(e)
})
})
},
link(q) {
return this.zHUrl + encodeURIComponent(q)
},
}
}
================================================
FILE: src/js/dictionary/collins.js
================================================
'use strict'
/**
* Dream Translate
* https://github.com/ryanker/dream_translate
* @Author Ryan
* @license MIT License
*/
function collinsDictionary() {
return {
// enUrl: 'https://www.collinsdictionary.com/dictionary/english/',
enUrl: 'https://www.collinsdictionary.com/search/?dictCode=english&q=',
init() {
return this
},
unify(r, q) {
let s = ''
let part = ''
let phonetic = {}
let sound = []
let el = r.querySelector('.page')
// 视频
let videoEl = el.querySelector('#videos .youtube-video[data-embed]')
if (videoEl) {
part += ``
}
// 图片
let imgEl = el.querySelector('#images img[data-image]')
if (imgEl) {
part += ``
}
// 释义
let dEl = el.querySelectorAll('.dictionaries > .dictentry')
if (dEl && dEl.length > 0) {
dEl.forEach(vEl => {
// 类型
let type = ''
let tEl = vEl.querySelector('.title_container .dictname')
if (tEl) {
let tStr = tEl.innerText
if (tStr.includes('in British English')) type = 'uk'
else if (tStr.includes('in American English')) type = 'us'
else type = 'en'
}
// 音标
let pEl = vEl.querySelector('.mini_h2 .pron')
if (pEl) {
let pStr = pEl.innerText && pEl.innerText.trim()
if (pStr) {
if (type === 'uk') phonetic.uk = pStr
else if (type === 'us') phonetic.us = pStr
}
let srcEl = pEl.querySelector('a[data-src-mp3]')
if (srcEl && ['uk', 'us'].includes(type)) {
let url = srcEl.getAttribute('data-src-mp3')
sound.push({type, url})
}
}
vEl.querySelectorAll('a.share-button,.share-overlay,.popup-overlay').forEach(e => e.remove())
vEl.querySelectorAll('.word-frequency-img > .roundRed').forEach(e => addClass(e, 'dmx-icon dmx-icon-star'))
// 喇叭
vEl.querySelectorAll('[data-src-mp3]').forEach(e => {
e.className = 'dmx-icon dmx_ripple'
e.setAttribute('data-type', 'en')
})
cleanAttr(vEl, ['title', 'class', 'href', 'data-src-mp3', 'data-type'])
vEl.querySelectorAll('a[href]').forEach(e => {
if (e.href.includes('/dictionary/english/')) e.setAttribute('data-search', 'true')
e.setAttribute('_href', e.href)
e.removeAttribute('href')
})
part += vEl.innerHTML
})
}
if (part) s += `
${part}
`
if (phonetic.us && phonetic.uk === phonetic.us) delete phonetic.us // 如果音标一样,只保留一个
return {text: q, phonetic, sound, html: s}
},
query(q) {
return new Promise((resolve, reject) => {
if (q.length > 100) return reject('The text is too large!')
let url = this.enUrl + encodeURIComponent(q)
httpGet(url, 'document').then(r => {
if (r) {
resolve(this.unify(r, q))
} else {
reject('collinsdictionary.com error!')
}
}).catch(e => {
reject(e)
})
})
},
link(q) {
return this.enUrl + encodeURIComponent(q)
},
}
}
================================================
FILE: src/js/dictionary/dictcn.js
================================================
'use strict'
/**
* Dream Translate
* https://github.com/ryanker/dream_translate
* @Author Ryan
* @license MIT License
*/
function dictcnDictionary() {
return {
init() {
return this
},
unify(r, q) {
let el = r.querySelector('#content > .main')
let s = ''
// 查询单词
let wordEl = el.querySelector('.word-cont > .keyword')
if (wordEl) s = `
${wordEl.innerText}
`
let phonetic = {} // 音标
let sound = [] // 发音
el.querySelectorAll('.phonetic > span').forEach(spanEl => {
let spStr = spanEl.innerText || ''
let bdoEl = spanEl.querySelector('bdo')
let ph = bdoEl && bdoEl.innerText && bdoEl.innerText.replace(/(^\[|]$)/g, '')
let type = ''
if (spStr.includes('美')) {
type = 'us'
if (ph) phonetic.us = ph
} else {
type = 'uk'
if (ph) phonetic.uk = ph
}
spanEl.querySelectorAll('.sound').forEach(e => {
let title = e.getAttribute('title') || ''
let url = 'https://audio.dict.cn/' + e.getAttribute('naudio')
let isWoman = e.className && e.className.includes('fsound')
sound.push({type, title, url, isWoman})
})
})
if (phonetic.us && phonetic.uk === phonetic.us) delete phonetic.us // 如果音标一样,只保留一个
// 释义
let partStr = ''
let liEl = el.querySelectorAll('.basic ul li')
if (liEl && liEl.length > 0) {
liEl.forEach(e => {
let bEl = e.querySelector('span')
let tEl = e.querySelector('strong')
let bStr = bEl && bEl.innerText ? `${bEl.innerText.trim()}` : ''
let part = tEl && tEl.innerText ? tEl.innerText.trim() : ''
if (part) partStr += `
${bStr}${part}
`
})
} else {
let liEl = el.querySelectorAll('.layout ul li')
if (liEl && liEl.length > 0) {
liEl.forEach(e => {
let part = e.innerText && e.innerText.trim()
if (part) partStr += `
`
let getChart = function (sel) {
try {
let e = el.querySelector(sel)
if (!e) return
let d = e.getAttribute('data')
d = decodeURIComponent(d)
d = JSON.parse(d)
let arr = Object.values(d)
if (arr && arr.length > 0) {
let str = ''
for (let v of arr) {
let {sense, percent, pos} = v
str += `${sense || pos || ''}${percent}%`
}
if (str) s += `
${str}
`
}
} catch (e) {
}
}
getChart('#dict-chart-basic') // 单词常用度
getChart('#dict-chart-examples') // 词性常用度
// 单词形态
let shapeEl = el.querySelector('.shape')
if (shapeEl) {
let shapeStr = ''
shapeEl.querySelectorAll('label,a').forEach(e => {
if (e.tagName === 'LABEL') {
shapeStr += `${e.innerText}`
} else if (e.tagName === 'A') {
shapeStr += `${e.innerText}`
}
})
if (shapeStr) s += `
${shapeStr}
`
}
// 单词标签
let levelEl = el.querySelector('span.level-title')
if (levelEl) {
let level = levelEl.getAttribute('level') || ''
if (level) s += `
${level}
`
}
return {text: q, phonetic, sound, html: s}
},
query(q) {
return new Promise((resolve, reject) => {
if (q.length > 100) return reject('The text is too large!')
httpGet(`https://dict.cn/${encodeURIComponent(q)}`, 'document', null, true).then(r => {
if (r) {
resolve(this.unify(r, q))
} else {
reject('dict.cn error!')
}
}).catch(e => {
reject(e)
})
})
},
link(q) {
return `https://dict.cn/${encodeURIComponent(q)}`
},
}
}
================================================
FILE: src/js/dictionary/dictionary.js
================================================
'use strict'
/**
* Dream Translate
* https://github.com/ryanker/dream_translate
* @Author Ryan
* @license MIT License
*/
function dictionaryDictionary() {
return {
url: 'https://www.dictionary.com/browse/',
init() {
return this
},
unify(r, q) {
let el = r.querySelector('#base-pw > main > section > section > div')
// 音标
let phonetic = {}
let pronEl = r.querySelector('.pron-spell-content')
if (pronEl) phonetic.us = pronEl.textContent.replace(/\[|]/g, '').trim()
// 发音
let sound = []
let soundEl = r.querySelector('source[type="audio/mpeg"]')
if (soundEl) sound.push({type: 'us', url: soundEl.src})
removeD(el.querySelectorAll('script,style,img,#top-definitions-section,.expandable-control')) // 清理
cleanAttr(el, ['title'])
return {text: q, phonetic, sound, html: el.innerHTML}
},
query(q) {
return new Promise((resolve, reject) => {
if (q.length > 100) return reject('The text is too large!')
let url = this.url + encodeURIComponent(q)
httpGet(url, 'document', null, true).then(r => {
if (r) {
resolve(this.unify(r, q))
} else {
reject('dictionary.com error!')
}
}).catch(e => {
reject(e)
})
})
},
link(q) {
return this.url + encodeURIComponent(q)
},
}
}
================================================
FILE: src/js/dictionary/dreye.js
================================================
'use strict'
/**
* Dream Translate
* https://github.com/ryanker/dream_translate
* @Author Ryan
* @license MIT License
*/
function dreyeDictionary() {
return {
init() {
return this
},
unify(r, q) {
let el = r.querySelector('.q_middle')
let s = ''
// 查询单词
let wordEl = el.querySelector('#display_word > span')
if (wordEl) s = `
${wordEl.innerText.trim()}
`
let phonetic = {} // 音标
let phEl = el.querySelector('.q_m_left > .phonetic')
if (phEl) {
let phStr = phEl.innerText && phEl.innerText.trim()
if (phStr) {
// 这词典太老了,不想弄~~~
let ukArr = phStr.match(/DJ:\[(.+?)]/)
let usArr = phStr.match(/KK:\[(.+?)]/)
if (ukArr && ukArr.length === 2) phonetic.uk = ukArr[1]
if (usArr && usArr.length === 2) phonetic.us = usArr[1]
if (phonetic.uk === phonetic.us) delete phonetic.us // 如果音标一样,只保留一个
}
}
let sound = [] // 发音
let scrEl = r.querySelector('script[language="javascript"]')
if (scrEl) {
let scrStr = scrEl.textContent || ''
let ukArr = scrStr.match(/var RealSoundPath = "(.+?)";/)
let usArr = scrStr.match(/var F_RealSoundPath = "(.+?)";/)
let roUrl = 'https://www.dreye.com.cn'
if (ukArr && ukArr.length === 2) sound.push({type: 'uk', url: roUrl + ukArr[1]})
if (usArr && usArr.length === 2) sound.push({type: 'us', url: roUrl + usArr[1]})
}
// 释义
let liEl = el.querySelectorAll('#digest > ul > li')
let partStr = ''
if (liEl && liEl.length > 0) {
liEl.forEach(e => {
let part = e.innerText && e.innerText.trim()
part = part.replace(/^[a-zA-Z]+\.\s+/, function (str, k) {
return k === 0 ? `${str.trim()}` : str
})
if (part) partStr += `
${part}
`
})
} else {
el.querySelectorAll('.q_middle_bd > .ews_sys_msg').forEach(e => {
let str = e.textContent
if (str) {
str = str.replace(/[a-z]+'?[a-z]+/ig, function (str) {
return `${str}`
})
partStr += `
${str}
`
}
})
}
if (partStr) s += `
${partStr}
`
// 单词形态
let pEl = el.querySelectorAll('#digest > p')
if (pEl && pEl.length > 0) {
let str = ''
pEl.forEach(e => {
let s2 = e.innerText.trim()
s2 = s2.replace(/[a-z]+/ig, function (s3) {
return `${s3}`
})
if (s2) str += `
${s2}
`
})
if (str) s += `
${str}
`
}
return {text: q, phonetic, sound, html: s}
},
query(q) {
return new Promise((resolve, reject) => {
if (q.length > 100) return reject('The text is too large!')
let url = `https://www.dreye.com.cn/dict_new/dict.php?w=${encodeURIComponent(q)}`
httpGet(url, 'document', null, true).then(r => {
if (r) {
resolve(this.unify(r, q))
} else {
reject('dreye.com.cn error!')
}
}).catch(e => {
reject(e)
})
})
},
link(q) {
return `https://www.dreye.com.cn/dict_new/dict.php?w=${encodeURIComponent(q)}`
},
}
}
================================================
FILE: src/js/dictionary/etymonline.js
================================================
'use strict'
/**
* Dream Translate
* https://github.com/ryanker/dream_translate
* @Author Ryan
* @license MIT License
*/
function etymonlineDictionary() {
return {
url: 'https://www.etymonline.com/word/',
// url: 'https://www.etymonline.com/search?q=',
init() {
return this
},
unify(r, q) {
// let el = r.querySelector('#root > div > div > div.main > div > div:nth-child(2) > div:nth-child(2) > object')
let s = ''
r.querySelectorAll('#root div.main div[class^="word--"]').forEach(el => {
cleanAttr(el, ['title', 'class'])
s += el.innerHTML
})
if (!s) s += `The ${q} you're looking for can't be found.`
return {text: q, phonetic: {}, sound: [], html: `
${s}
`}
},
query(q) {
return new Promise((resolve, reject) => {
if (q.length > 100) return reject('The text is too large!')
let url = this.url + encodeURIComponent(q)
httpGet(url, 'document', null, true).then(r => {
if (r) {
resolve(this.unify(r, q))
} else {
reject('etymonline.com error!')
}
}).catch(e => {
reject(e)
})
})
},
link(q) {
return this.url + encodeURIComponent(q)
},
}
}
================================================
FILE: src/js/dictionary/eudic.js
================================================
'use strict'
/**
* Dream Translate
* https://github.com/ryanker/dream_translate
* @Author Ryan
* @license MIT License
*/
function eudicDictionary() {
return {
init() {
return this
},
unify(r, q) {
let s = ''
let phonetic = {} // 音标
let sound = [] // 发音
let el = r.querySelector('#dict-body')
// 查询单词
let wordEl = el.querySelector('h1.explain-Word .word')
if (wordEl) s = `
${wordEl.innerText.trim()}
`
el.querySelectorAll('.phonitic-line .Phonitic').forEach((e, k) => {
let ph = e.innerText && e.innerText.replace(/\//g, '').trim() || ''
if (!ph) return
if (k === 0) phonetic.uk = ph
else phonetic.us = ph
})
if (phonetic.us && phonetic.uk === phonetic.us) delete phonetic.us // 如果音标一样,只保留一个
el.querySelectorAll('.phonitic-line a.voice-button-en').forEach(e => {
let rel = e.getAttribute('data-rel')
if (!rel) return
let type = 'en'
if (rel.includes('_uk_')) type = 'uk'
else if (rel.includes('_us_')) type = 'us'
sound.push({type, url: 'https://api.frdic.com/api/v2/speech/speakweb?' + rel})
})
// 释义
let liEl = el.querySelectorAll('#ExpFCChild li')
if (liEl && liEl.length > 0) {
let str = ''
liEl.forEach(e => {
let tex = e.innerText && e.innerText.trim()
tex = tex.replace(/^[a-zA-Z]+\.\s+/, function (str, k) {
return k === 0 ? `${str.trim()}` : str
})
if (tex) str += `
${tex}
`
})
if (str) s += `
${str}
`
} else {
let expEl = el.querySelector('#ExpFCChild')
if (expEl) {
expEl.querySelectorAll('script,style,#word-thumbnail-image').forEach(e => e.remove())
cleanAttr(expEl, ['title', 'class'])
if (expEl.innerHTML) s += `
${expEl.innerHTML}
`
}
}
// 单词形态
let transEl = el.querySelector('#trans')
if (transEl) {
let shapeStr = transEl.innerText.trim()
shapeStr = shapeStr.replace(/[a-z]+/ig, function (str) {
return `${str}`
})
s += `
${shapeStr}
`
}
return {text: q, phonetic, sound, html: s}
},
query(q) {
return new Promise((resolve, reject) => {
if (q.length > 100) return reject('The text is too large!')
let url = `https://dict.eudic.net/dicts/en/${encodeURIComponent(q)}`
httpGet(url, 'document', null, true).then(r => {
if (r) {
resolve(this.unify(r, q))
} else {
reject('dict.eudic.net error!')
}
}).catch(e => {
reject(e)
})
})
},
link(q) {
return `https://dict.eudic.net/dicts/en/${encodeURIComponent(q)}`
},
}
}
================================================
FILE: src/js/dictionary/hjdict.js
================================================
'use strict'
/**
* Dream Translate
* https://github.com/ryanker/dream_translate
* @Author Ryan
* @license MIT License
*/
function hjdictDictionary() {
return {
isFirst: true,
init() {
return this
},
unify(r, q) {
let el = r.querySelector('.word-details')
let s = ''
let phonetic = {} // 音标
let sound = [] // 发音
// 没有找到结果
if (!el) {
let notEl = r.querySelector('.word-notfound-inner h2')
if (notEl) return {text: q, phonetic, sound, html: notEl.innerText, error: true}
}
// 查询单词
let wordEl = el.querySelector('.word-info .word-text h2')
if (wordEl) s = `
${wordEl.innerText.trim()}
`
let pronEl = el.querySelector('.word-info .pronounces')
if (pronEl && pronEl.childNodes.length > 0) {
let ukEl = pronEl.querySelector('.pronounce-value-en')
let usEl = pronEl.querySelector('.pronounce-value-us')
let auEl = pronEl.querySelectorAll('.word-audio')
if (ukEl && ukEl.innerText) phonetic.uk = ukEl.innerText.replace(/[\[\]]/g, '').trim()
if (usEl && usEl.innerText) phonetic.us = usEl.innerText.replace(/[\[\]]/g, '').trim()
if (auEl && auEl.length === 2) {
auEl.forEach((e, k) => {
let type = k === 0 ? 'uk' : 'us'
let url = e.dataset.src
if (url) sound.push({type, url})
})
} else {
auEl.forEach(e => {
let type = e.className.includes('word-audio-us') ? 'us' : e.className.includes('word-audio-en') ? 'uk' : 'us'
let url = e.dataset.src
if (url) sound.push({type, url})
})
}
}
if (phonetic.us && phonetic.uk === phonetic.us) delete phonetic.us // 如果音标一样,只保留一个
// 释义
let liEl = el.querySelectorAll('.simple > p')
if (liEl && liEl.length > 0) {
s += `
`
liEl.forEach(e => {
let part = e.innerText && e.innerText.trim()
part = part.replace(/^[a-zA-Z]+\.\s+/, function (str, k) {
return k === 0 ? `${str.trim()}` : str
})
if (part) s += `
${part}
`
})
s += `
`
}
// 详细释义
/*let detailEl = el.querySelectorAll('.word-details-item-content > .detail-groups')
if (detailEl && detailEl.length > 0) {
detailEl.forEach(e => {
let str = e.innerHTML && e.innerHTML.trim()
if (str) s += str
})
}*/
return {text: q, phonetic, sound, html: s}
},
query(q) {
return new Promise((resolve, reject) => {
if (q.length > 100) return reject('The text is too large!')
let url = `https://www.hjdict.com/w/${encodeURIComponent(q)}`
httpGet(url, 'document', null, true).then(r => {
if (r) {
resolve(this.unify(r, q))
} else {
reject('hjdict.com empty!')
}
}).catch(e => {
reject(e)
})
})
},
link(q) {
return `https://www.hjdict.com/w/${encodeURIComponent(q)}`
},
}
}
================================================
FILE: src/js/dictionary/iciba.js
================================================
'use strict'
/**
* Dream Translate
* https://github.com/ryanker/dream_translate
* @Author Ryan
* @license MIT License
*/
function icibaDictionary() {
return {
init() {
return this
},
unify(r, q) {
let s = ''
let phonetic = {} // 音标
let sound = [] // 发音
let el = r.querySelector('#__next > main > div > div[class^=Content_center]')
// JSON 数据
let data = {}
let basic = {}
let jEl = r.querySelector('#__NEXT_DATA__')
if (jEl) {
try {
data = JSON.parse(jEl.textContent)
if (data) basic = getJSONValue(data, 'props.pageProps.initialReduxState.word.wordInfo.baesInfo', {})
} catch (e) {
}
}
// 查询单词
let wordEl = el.querySelector('h1')
if (wordEl) s = `
${wordEl.innerText.trim()}
`
el.querySelectorAll('ul[class^=Mean_symbols] > li').forEach(e => {
let ph = e.innerText && e.innerText.replace(/[\[\]英美]/g, '').trim() || ''
let type = ''
if (e.innerText.includes('美')) {
if (ph) phonetic.us = ph
type = 'us'
} else {
if (ph) phonetic.uk = ph
type = 'uk'
}
// 发音
let symbols = getJSONValue(basic, 'symbols.0')
if (symbols) {
let url = ''
let {ph_am_mp3, ph_am_mp3_bk, ph_en_mp3, ph_en_mp3_bk, ph_tts_mp3, ph_tts_mp3_bk} = symbols
if (type === 'us') {
url = ph_am_mp3 || ph_am_mp3_bk || ph_tts_mp3_bk
} else {
url = ph_en_mp3 || ph_en_mp3_bk || ph_tts_mp3
}
if (url) sound.push({type, url})
}
})
if (phonetic.us && phonetic.uk === phonetic.us) delete phonetic.us // 如果音标一样,只保留一个
// 释义
let liEl = el.querySelectorAll('ul[class^=Mean_part] > li')
if (liEl && liEl.length > 0) {
s += `
`
liEl.forEach(e => {
let bEl = e.querySelector('i')
let tEl = e.querySelector('div')
let bStr = bEl && bEl.innerText ? `${bEl.innerText.trim()}` : ''
let part = tEl && tEl.innerText ? tEl.innerText.trim() : ''
if (bStr && part) {
s += `
${bStr}${part}
`
} else {
let part = e.innerText && e.innerText.trim()
if (part) s += `
${part}
`
}
})
s += `
`
} else {
let str = ''
let senEl = el.querySelector('h2[class^=Mean_sentence]')
if (senEl) str += `
${senEl.textContent}
`
let transEl = el.querySelector('div[class^=Mean_trans] > p')
if (transEl) str += `
${transEl.textContent}
`
if (str) s += `
${str}
`
}
// 单词形态
let shapeEl = el.querySelector('ul[class^=Morphology_morphology]')
if (shapeEl) {
let shapeStr = shapeEl.innerText.trim()
shapeStr = shapeStr.replace(/;/g, ' ')
shapeStr = shapeStr.replace(/[a-z]+/ig, function (str) {
return `${str}`
})
s += `
${shapeStr}
`
}
return {text: q, phonetic, sound, html: s}
},
query(q) {
return new Promise((resolve, reject) => {
// if (q.length > 100) return reject('The text is too large!')
let url = `https://www.iciba.com/word?w=${encodeURIComponent(q)}`
httpGet(url, 'document').then(r => {
if (r) {
resolve(this.unify(r, q))
} else {
reject('iciba.com error!')
}
}).catch(e => {
reject(e)
})
})
},
link(q) {
return `https://www.iciba.com/word?w=${encodeURIComponent(q)}`
},
}
}
================================================
FILE: src/js/dictionary/lexico.js
================================================
'use strict'
/**
* Dream Translate
* https://github.com/ryanker/dream_translate
* @Author Ryan
* @license MIT License
*/
function lexicoDictionary() {
return {
ukUrl: 'https://www.thefreedictionary.com/',
usUrl: 'https://www.lexico.com/en/definition/',
init() {
return this
},
unify(r, q) {
let s = ''
let phonetic = {}
let sound = []
let el = r.querySelector('.entryWrapper')
// 查询单词
let wordEl = el.querySelector('.hwg > .hw')
if (wordEl) s = `
${wordEl.innerText.trim()}
`
// 音标
let pronEl = el.querySelector('.pronunciations .phoneticspelling')
if (pronEl) {
let pron = pronEl.innerText && pronEl.innerText.replace(/\//g, '')
if (pron) phonetic.us = pron
}
// 发音
let mp3El = el.querySelector('.pronunciations audio[src]')
if (mp3El) sound.push({type: 'us', url: mp3El.src})
// 释义
let liEl = el.querySelectorAll('.gramb')
if (liEl && liEl.length > 0) {
liEl.forEach(e => {
removeD(e.querySelectorAll('script,style,.moreInfo')) // 清理
cleanAttr(e, ['title', 'class'])
s += e.innerHTML
})
} else {
let simEl = el.querySelector('.similar-results')
if (simEl) {
cleanAttr(simEl, ['title', 'class', 'href'])
simEl.querySelectorAll('a[href]').forEach(e => {
e.setAttribute('data-search', 'true')
e.removeAttribute('href')
})
s += simEl.innerHTML
}
}
return {text: q, phonetic, sound, html: s}
},
query(q) {
return new Promise((resolve, reject) => {
if (q.length > 100) return reject('The text is too large!')
let url = this.usUrl + encodeURIComponent(q)
httpGet(url, 'document').then(r => {
if (r) {
resolve(this.unify(r, q))
} else {
reject('lexico.com error!')
}
}).catch(e => {
reject(e)
})
})
},
link(q) {
return this.usUrl + encodeURIComponent(q)
},
}
}
================================================
FILE: src/js/dictionary/longman.js
================================================
'use strict'
/**
* Dream Translate
* https://github.com/ryanker/dream_translate
* @Author Ryan
* @license MIT License
*/
function longmanDictionary() {
return {
init() {
return this
},
unify(r, q) {
// console.log(r)
// let arr = r.match(/
([\s\S]*?)<\/div>/m)
// console.log(arr)
let el = r.querySelector('.dictionary')
if (!el) {
el = r.querySelector('.page_content')
if (!el) return {text: q, html: 'Failed to get data!'}
let html = ''
let tEl = el.querySelector('.search_title')
if (tEl) html += `
${tEl.innerText}
`
el.querySelectorAll('.didyoumean > li > a').forEach(e => {
let href = e.getAttribute('href')
html += `
${e.innerText}
`
})
return {text: q, html: html}
}
// 音标
let headEl = el.querySelector('.Head')
headEl.querySelector('.PronCodes > .AMEVARPRON > .neutral')?.remove()
let ukPron = headEl.querySelector('.PronCodes > .PRON')?.innerText?.trim()
let usPron = headEl.querySelector('.PronCodes > .AMEVARPRON')?.innerText?.trim()
headEl.querySelector('.PronCodes')?.remove()
let phonetic = {}
if (ukPron) phonetic.uk = ukPron
if (usPron) phonetic.us = usPron
// 发音
let sound = []
headEl.querySelectorAll('[data-src-mp3]').forEach(e => {
let title = e.getAttribute('title')
let src = e.getAttribute('data-src-mp3')
if (title && src) {
if (title.includes('British')) sound.push({type: 'uk', title: title, url: src})
else if (title.includes('American')) sound.push({type: 'us', title: title, url: src})
}
e.remove()
})
// 喇叭
el.querySelectorAll('[data-src-mp3]').forEach(e => {
e.className = 'dmx-icon dmx_ripple'
let v = 'en'
let title = e.getAttribute('title')
if (title) {
if (title.includes('British')) v = 'uk'
else if (title.includes('American')) v = 'us'
}
e.setAttribute('data-type', v)
})
// 图片
el.querySelectorAll('img').forEach(e => {
e.setAttribute('referrerPolicy', 'no-referrer')
})
// 链接
el.querySelectorAll('a').forEach(e => {
let href = e.getAttribute('href')
let s = q.replace(/\W/g, '-')
if (e.className === 'crossRef' && href.includes(`/dictionary/${s}#${s}`)) {
e.remove() // 清理掉本页跳转链接
return
}
e.setAttribute('_href', href)
e.removeAttribute('href')
e.setAttribute('data-search', 'true')
})
// 清理
el.querySelectorAll('[id]').forEach(e => {
e.removeAttribute('id')
})
el.querySelectorAll('script,style,.dictionary_intro').forEach(e => {
e.remove()
})
el.className = 'longman_dict'
return {text: q, phonetic: phonetic, sound: sound, html: el.outerHTML}
},
query(q) {
return new Promise((resolve, reject) => {
if (q.length > 100) return reject('The text is too large!')
let url = `https://www.ldoceonline.com/search/english/direct/?q=${encodeURIComponent(q)}`
// q = q.trim().replace(/\s+/g, ' ').replace(/\W/g, '-')
// let url = `https://www.ldoceonline.com/dictionary/${q}`
httpGet(url, 'document').then(r => {
if (r) {
resolve(this.unify(r, q))
} else {
reject('longman error!')
}
}).catch(e => {
reject(e)
})
})
},
link(q) {
// return `https://www.ldoceonline.com/dictionary/${encodeURIComponent(q)}`
return `https://www.ldoceonline.com/search/english/direct/?q=${encodeURIComponent(q)}`
},
}
}
================================================
FILE: src/js/dictionary/macmillan.js
================================================
'use strict'
/**
* Dream Translate
* https://github.com/ryanker/dream_translate
* @Author Ryan
* @license MIT License
*/
function macmillanDictionary() {
return {
url: 'https://www.macmillandictionary.com/search/british/direct/?auto=complete&q=',
init() {
return this
},
unify(r, q) {
let s = ''
let phonetic = {}
let sound = []
let el = r.querySelector('#entryContent > div > div.left-content')
// 查询单词
let wordEl = el.querySelector('.big-title .BASE')
if (wordEl) s = `
${wordEl.innerText.trim()}
`
// 音标
let pronEl = r.querySelector('.PRON')
if (pronEl) {
pronEl.querySelectorAll('span').forEach(e => e.remove())
let pronStr = pronEl.textContent && pronEl.textContent.trim()
if (pronStr) phonetic.uk = pronStr
}
// 发音
let soundEl = r.querySelector('.PRONS span[data-src-mp3]')
if (soundEl) sound.push({type: 'uk', url: soundEl.dataset.srcMp3})
// 释义
let sensesEl = el.querySelector('.senses')
if (sensesEl) {
removeD(sensesEl.querySelectorAll('script,style,button'))
cleanAttr(sensesEl, ['title', 'class', 'href'])
sensesEl.querySelectorAll('a[href]').forEach(e => {
// e.setAttribute('data-search', 'true')
e.removeAttribute('href')
})
s += sensesEl.innerHTML
}
return {text: q, phonetic, sound, html: `${s}`}
},
query(q) {
return new Promise((resolve, reject) => {
if (q.length > 100) return reject('The text is too large!')
let url = this.url + encodeURIComponent(q)
httpGet(url, 'document').then(r => {
if (r) {
resolve(this.unify(r, q))
} else {
reject('macmillandictionary.com error!')
}
}).catch(e => {
reject(e)
})
})
},
link(q) {
return this.url + encodeURIComponent(q)
},
}
}
================================================
FILE: src/js/dictionary/merriam.js
================================================
'use strict'
/**
* Dream Translate
* https://github.com/ryanker/dream_translate
* @Author Ryan
* @license MIT License
*/
function merriamDictionary() {
return {
dUrl: 'https://www.learnersdictionary.com/definition/',
mUrl: 'https://www.merriam-webster.com/dictionary/',
mp3Url: 'https://media.merriam-webster.com/audio/prons/en/us/mp3',
init() {
return this
},
getMp3(file) {
return `${this.mp3Url}/${file.substring(0, 1)}/${file}.mp3`
},
unify(r, q) {
let s = ''
let phonetic = {}
let sound = []
let el = r.querySelector('#ld_entries_v2_all')
// 音标
let pronEl = el.querySelector('.hpron_word')
if (pronEl) {
let pron = pronEl.textContent && pronEl.textContent.replace(/\//g, '').trim()
if (pron) phonetic.uk = pron
}
// 发音
let pEl = el.querySelector('a.play_pron[data-file]')
if (pEl) {
let file = pEl.getAttribute('data-file')
sound.push({type: 'us', url: this.getMp3(file)})
}
// 释义
let part = ''
let deepCommentRemove = function (el) {
for (let v of el.childNodes) {
if (v.nodeType === 8) v.remove() // 删除注释
if (v.childNodes.length > 0) deepCommentRemove(v)
}
}
let v2El = el.querySelectorAll('.entry_v2')
if (v2El && v2El.length > 0) {
v2El.forEach(vEl => {
deepCommentRemove(vEl)
vEl.querySelectorAll('.vi_more,.d_hidden').forEach(e => e.remove())
// 喇叭
vEl.querySelectorAll('a.play_pron[data-file]').forEach(e => {
e.className = 'dmx-icon dmx_ripple'
let file = e.getAttribute('data-file')
e.setAttribute('data-src-mp3', this.getMp3(file))
e.setAttribute('data-type', 'us')
})
cleanAttr(vEl, ['title', 'class', 'href', 'data-type', 'data-src-mp3'])
vEl.querySelectorAll('a[href]').forEach(e => {
if (e.href.includes('/definition/') && !e.getAttribute('data-src-mp3')) {
e.setAttribute('data-search', 'true')
}
e.setAttribute('_href', e.href)
e.removeAttribute('href')
})
vEl.querySelectorAll('.sn_block_num').forEach(e => e.style.float = 'left')
vEl.querySelectorAll('.sblock_c').forEach(e => e.style.marginTop = '10px')
vEl.querySelectorAll('.sgram_internal').forEach(e => e.style.color = '#757575')
vEl.querySelectorAll('.hw_d').forEach(e => {
e.style.color = '#0580e8'
e.style.fontSize = '110%'
})
part += vEl.innerHTML.replace(/\s+/g, ' ')
})
}
if (part) s += `
${part}
`
return {text: q, phonetic, sound, html: s}
},
query(q) {
return new Promise((resolve, reject) => {
if (q.length > 100) return reject('The text is too large!')
let url = this.dUrl + encodeURIComponent(q)
httpGet(url, 'document').then(r => {
if (r) {
resolve(this.unify(r, q))
} else {
reject('merriam-webster.com error!')
}
}).catch(e => {
reject(e)
})
})
},
link(q) {
return this.dUrl + encodeURIComponent(q)
},
}
}
================================================
FILE: src/js/dictionary/oxford.js
================================================
'use strict'
/**
* Dream Translate
* https://github.com/ryanker/dream_translate
* @Author Ryan
* @license MIT License
*/
function oxfordDictionary() {
return {
url: 'https://www.oxfordlearnersdictionaries.com/search/english/?q=',
init() {
return this
},
unify(r, q) {
let s = ''
let phonetic = {}
let sound = []
let el = r.querySelector('#entryContent')
if (!el) {
let el = r.querySelector('#main_column')
cleanAttr(el, ['title', 'class', 'href'])
el.querySelectorAll('a[href]').forEach(e => {
e.setAttribute('data-search', 'true')
e.removeAttribute('href')
})
return {text: q, phonetic, sound, html: el.innerHTML}
}
// 查询单词
let wordEl = el.querySelector('.headword')
if (wordEl) s = `
${wordEl.innerText.trim()}
`
// 音标 && 发音
let ukEl = el.querySelector('.webtop .phonetics .phons_br')
let usEl = el.querySelector('.webtop .phonetics .phons_n_am')
if (ukEl) {
let pron = ukEl.textContent.replace(/\//g, '').trim()
if (pron) phonetic.uk = pron
let srcEl = ukEl.querySelector('.sound[data-src-mp3]')
if (srcEl) sound.push({type: 'uk', url: srcEl.getAttribute('data-src-mp3')})
}
if (usEl) {
let pron = usEl.textContent.replace(/\//g, '').trim()
if (pron) phonetic.us = pron
let srcEl = ukEl.querySelector('.sound[data-src-mp3]')
if (srcEl) sound.push({type: 'us', url: srcEl.getAttribute('data-src-mp3')})
}
// 释义
let part = ''
let sensesEl = el.querySelector('.senses_multiple')
if (!sensesEl) sensesEl = el.querySelector('.sense_single')
if (sensesEl) {
sensesEl.querySelectorAll('script,style,span.sensetop').forEach(e => e.remove()) // 清理
cleanAttr(sensesEl, ['title', 'class', 'href'])
el.querySelectorAll('a[href]').forEach(e => {
e.setAttribute('data-search', 'true')
e.removeAttribute('href')
})
part += sensesEl.innerHTML.replace(/\s+/g, ' ')
}
if (part) s += `
${part}
`
return {text: q, phonetic, sound, html: s}
},
query(q) {
return new Promise((resolve, reject) => {
if (q.length > 100) return reject('The text is too large!')
let url = this.url + encodeURIComponent(q)
httpGet(url, 'document', null, true).then(r => {
if (r) {
resolve(this.unify(r, q))
} else {
reject('oxfordlearnersdictionaries.com error!')
}
}).catch(e => {
reject(e)
})
})
},
link(q) {
return this.url + encodeURIComponent(q)
},
}
}
================================================
FILE: src/js/dictionary/rrdict.js
================================================
'use strict'
/**
* Dream Translate
* https://github.com/ryanker/dream_translate
* @Author Ryan
* @license MIT License
*/
function rrdictDictionary() {
return {
init() {
return this
},
unify(r, q) {
let el = r.querySelector('section.content')
let s = ''
// 查询单词
let wordEl = el.querySelector('.text')
if (wordEl) s = `
${wordEl.innerText.trim()}
`
let phonetic = {} // 音标
let sound = [] // 发音
el.querySelectorAll('.vos > span').forEach((e, k) => {
let ph = e.innerText && e.innerText.replace(/[\[\]英美]/g, '').trim() || ''
if (!ph) return
let type = ''
if (k === 0) {
phonetic.uk = ph
type = 'uk'
} else {
phonetic.us = ph
type = 'us'
}
let auEl = e.querySelector('audio[src]')
if (!auEl) return
sound.push({type, url: auEl.src})
})
if (phonetic.us && phonetic.uk === phonetic.us) delete phonetic.us // 如果音标一样,只保留一个
// 释义
let liEl = el.querySelector('.tmInfo .listBox')
let interStr = ''
if (liEl && liEl.childNodes.length > 0) {
let poEl = liEl.querySelector('.poBottom')
if (poEl) poEl.remove()
let liHtml = liEl.innerHTML.trim()
for (let part of liHtml.split(' ')) {
part = part.trim()
if (part.includes('${interStr}
`
// 单词形态
let transEl = el.querySelector('.tmInfo .listBox:nth-child(2)')
if (transEl) {
let shapeStr = transEl.innerText.trim()
shapeStr = shapeStr.replace(/[a-z]+/ig, function (str) {
return `${str}`
})
s += `
${shapeStr}
`
}
// 场景例句
let flexEl = el.querySelectorAll('#flexslider_2 ul li')
if (flexEl && flexEl.length > 0) {
s += `
`
flexEl.forEach(e => {
let imgBox = e.querySelector('.imgMainbox')
if (imgBox) {
// let imgEl = imgBox.querySelector('img[src]')
let auEl = imgBox.querySelector('audio[src]')
let enEl = imgBox.querySelector('.mBottom')
let zhEl = imgBox.querySelector('.mFoot')
let fromEl = imgBox.querySelector('.mTop')
let url = auEl.src
if (url && enEl && zhEl && fromEl) {
let form = fromEl.innerText ? ' —— ' + fromEl.innerText.trim() : ''
s += `
${enEl.innerHTML}
${zhEl.innerText}${form}
`
}
}
// e.querySelectorAll('.mTextend > .box').forEach(bEl => {
// let tEl = bEl.querySelector('.sty1')
// let cEl = bEl.querySelector('.sty2')
// })
})
s += `
`
}
// 单词标签
let tagEl = el.querySelectorAll('.tag > a')
if (tagEl && tagEl.length > 0) {
s += `
`
tagEl.forEach(e => {
let tag = e.innerText && e.innerText.trim()
if (tag) s += `${tag}`
})
s += `
`
}
return {text: q, phonetic, sound, html: s}
},
query(q) {
return new Promise((resolve, reject) => {
if (q.length > 100) return reject('The text is too large!')
let url = `https://www.91dict.com/words?w=${encodeURIComponent(q)}`
httpGet(url, 'document', null, true).then(r => {
if (r) {
resolve(this.unify(r, q))
} else {
reject('91dict.com error!')
}
}).catch(e => {
reject(e)
})
})
},
link(q) {
return `https://www.91dict.com/words?w=${encodeURIComponent(q)}`
},
}
}
================================================
FILE: src/js/dictionary/thefree.js
================================================
'use strict'
/**
* Dream Translate
* https://github.com/ryanker/dream_translate
* @Author Ryan
* @license MIT License
*/
function thefreeDictionary() {
return {
url: 'https://www.thefreedictionary.com/',
init() {
return this
},
unify(r, q) {
let s = ''
let phonetic = {}
let sound = []
let el = r.querySelector('#content')
// 音标
let pronEl = r.querySelector('span.pron')
if (pronEl) {
let pron = pronEl.innerText && pronEl.innerText.replace(/^\(|\)$/g, '')
if (pron) phonetic.uk = pron
}
// 发音
let ukEl = el.querySelector('.snd2[data-snd^="en/UK/"]')
let usEl = el.querySelector('.snd2[data-snd^="en/US/"]')
if (ukEl) sound.push({type: 'uk', url: `https://img2.tfd.com/pron/mp3/${ukEl.dataset.snd}.mp3`})
if (usEl) sound.push({type: 'us', url: `https://img2.tfd.com/pron/mp3/${usEl.dataset.snd}.mp3`})
let defEl = el.querySelector('#Definition')
if (defEl) {
removeD(defEl.querySelectorAll('script,style,select.verbtables')) // 清理
defEl.querySelectorAll('span.snd[data-snd]').forEach(e => {
e.className = 'dmx-icon dmx_ripple'
e.setAttribute('data-src-mp3', `https://img.tfd.com/hm/mp3/${e.dataset.snd}.mp3`)
e.setAttribute('data-type', 'en')
})
cleanAttr(defEl, ['title', 'class', 'href', 'data-snd', 'data-type', 'data-src-mp3'])
defEl.querySelectorAll('span[class="hvr"]').forEach(e => {
e.setAttribute('data-search', 'true')
})
defEl.querySelectorAll('a[href]').forEach(e => {
e.setAttribute('data-search', 'true')
e.setAttribute('_href', e.href)
e.removeAttribute('href')
})
s += defEl.innerHTML
} else {
let txtEl = r.querySelector('#MainTxt')
cleanAttr(txtEl, ['title', 'class', 'href'])
txtEl.querySelectorAll('a[href]').forEach(e => {
e.setAttribute('data-search', 'true')
e.removeAttribute('href')
})
s += txtEl.innerHTML
}
return {text: q, phonetic, sound, html: s}
},
query(q) {
return new Promise((resolve, reject) => {
if (q.length > 100) return reject('The text is too large!')
let url = this.url + encodeURIComponent(q)
httpGet(url, 'document').then(r => {
if (r) {
resolve(this.unify(r, q))
} else {
reject('thefreedictionary.com error!')
}
}).catch(e => {
reject(e)
})
})
},
link(q) {
return this.url + encodeURIComponent(q)
},
}
}
================================================
FILE: src/js/dictionary/urban.js
================================================
'use strict'
/**
* Dream Translate
* https://github.com/ryanker/dream_translate
* @Author Ryan
* @license MIT License
*/
function urbanDictionary() {
return {
url: 'https://www.urbandictionary.com/define.php?term=',
init() {
return this
},
unify(r, q) {
let el = r.querySelector('#content > .def-panel')
removeD(el.querySelectorAll('script,style,.row,.def-footer,a.mug-ad'))
cleanAttr(el, ['title', 'class', 'autoplay', 'loop', 'muted', 'playsinline', 'src', 'width', 'height'])
el.querySelectorAll('.def-header').forEach(e => {
e.style.fontSize = '120%'
e.style.fontWeight = '700'
})
return {text: q, phonetic: {}, sound: [], html: el.innerHTML}
},
query(q) {
return new Promise((resolve, reject) => {
if (q.length > 100) return reject('The text is too large!')
let url = this.url + encodeURIComponent(q)
httpGet(url, 'document', null, true).then(r => {
if (r) {
resolve(this.unify(r, q))
} else {
reject('urbandictionary.com error!')
}
}).catch(e => {
reject(e)
})
})
},
link(q) {
return this.url + encodeURIComponent(q)
},
}
}
================================================
FILE: src/js/dictionary/vocabulary.js
================================================
'use strict'
/**
* Dream Translate
* https://github.com/ryanker/dream_translate
* @Author Ryan
* @license MIT License
*/
function vocabularyDictionary() {
return {
url: 'https://www.vocabulary.com/dictionary/',
init() {
return this
},
unify(r, q) {
let el = r.querySelector('.centeredContent')
// 发音
let sound = []
let soundEl = r.querySelector('a.audio[data-audio]')
if (soundEl) sound.push({type: 'us', url: `https://audio.vocab.com/1.0/us/${soundEl.dataset.audio}.mp3`})
// 清理
removeD(el.querySelectorAll('script,style,img'))
cleanAttr(el, ['title', 'class'])
el.querySelectorAll('.groupNumber').forEach(e => {
e.style.marginTop = '10px'
e.style.fontWeight = '700'
})
return {text: q, phonetic: {}, sound, html: el.innerHTML}
},
query(q) {
return new Promise((resolve, reject) => {
if (q.length > 100) return reject('The text is too large!')
let url = this.url + encodeURIComponent(q)
httpGet(url, 'document', null, true).then(r => {
if (r) {
resolve(this.unify(r, q))
} else {
reject('vocabulary.com error!')
}
}).catch(e => {
reject(e)
})
})
},
link(q) {
return this.url + encodeURIComponent(q)
},
}
}
================================================
FILE: src/js/dictionary/wordreference.js
================================================
'use strict'
/**
* Dream Translate
* https://github.com/ryanker/dream_translate
* @Author Ryan
* @license MIT License
*/
function wordreferenceDictionary() {
return {
url: 'https://www.wordreference.com/definition/',
init() {
return this
},
unify(r, q) {
let s = ''
let el = r.querySelector('#centercolumn')
// 发音
let sound = []
let ukEl = r.querySelector('source[src^="/audio/en/uk"]')
let usEl = r.querySelector('source[src^="/audio/en/us"]')
if (ukEl) sound.push({type: 'uk', url: ukEl.src})
if (usEl) sound.push({type: 'us', url: usEl.src})
// 清理
let artEl = el.querySelector('#article')
if (artEl) {
removeD(artEl.querySelectorAll('script,style,img,br,.small1'))
cleanAttr(artEl, ['title', 'class'])
let artStr = artEl.innerHTML
artStr = artStr.trim()
artStr = artStr.replace(/^\s* \s* /g, '')
s += artStr
}
return {text: q, phonetic: {}, sound, html: `
${s}
`}
},
query(q) {
return new Promise((resolve, reject) => {
if (q.length > 100) return reject('The text is too large!')
let url = this.url + encodeURIComponent(q)
httpGet(url, 'document').then(r => {
if (r) {
resolve(this.unify(r, q))
} else {
reject('wordreference.com error!')
}
}).catch(e => {
reject(e)
})
})
},
link(q) {
return this.url + encodeURIComponent(q)
},
}
}
================================================
FILE: src/js/dictionary/youdao.js
================================================
'use strict'
/**
* Dream Translate
* https://github.com/ryanker/dream_translate
* @Author Ryan
* @license MIT License
*/
function youdaoDictionary() {
return {
init() {
return this
},
unify(r, text) {
let s = ''
let phonetic = {} // 音标
let sound = [] // 发音
let el = r.querySelector('#results-contents')
// 查询单词
let wordEl = el.querySelector('.wordbook-js .keyword')
if (wordEl) s = `
${wordEl.innerText}
`
el.querySelectorAll('.wordbook-js .baav .pronounce').forEach(e => {
let pEl = e.querySelector('.phonetic')
let aEl = e.querySelector('a')
if (!aEl) return
let phStr = pEl && pEl.innerText && pEl.innerText.replace(/(^\[|]$)/g, '')
let rel = aEl.getAttribute('data-rel') || ''
let voice = aEl.getAttribute('data-4log') || ''
let url = 'https://dict.youdao.com/dictvoice?audio=' + rel
let type = ''
if (voice.includes('.uk.')) {
type = 'uk'
if (phStr) phonetic.uk = phStr
} else if (voice.includes('.us.')) {
type = 'us'
if (phStr) phonetic.us = phStr
} else {
type = 'en'
if (phStr) phonetic.uk = phStr
}
sound.push({type, url})
})
if (phonetic.us && phonetic.uk === phonetic.us) delete phonetic.us // 如果音标一样,只保留一个
// 释义
let transEl = el.querySelector('#phrsListTab .trans-container')
if (transEl) {
let str = ''
let liEl = transEl.querySelectorAll('li')
if (liEl && liEl.length > 0) {
liEl.forEach(e => {
let part = e.innerText && e.innerText.trim()
part = part.replace(/^[a-zA-Z]+\.\s+/, function (str, k) {
return k === 0 ? `${str.trim()}` : str
})
if (part) str += `
${part}
`
})
} else {
let wordEl = el.querySelector('.wordGroup')
if (wordEl) {
let part = wordEl.innerText && wordEl.innerText.trim()
part = part.replace(/^[a-zA-Z]+\.\s+/, function (str, k) {
return k === 0 ? `${str.trim()}` : str
})
if (part) str += `
${part}
`
}
}
if (str) s += `
${str}
`
// 单词形态
let addiEl = transEl.querySelector('.additional')
if (addiEl) {
let shapeStr = addiEl.innerText.trim()
shapeStr = shapeStr.replace(/^\[|]$/g, '')
shapeStr = shapeStr.trim()
shapeStr = shapeStr.replace(/[a-z]+/ig, function (str) {
return `${str}`
})
s += `
${shapeStr}
`
}
} else {
let transEl = el.querySelector('#fanyiToggle .trans-container')
if (transEl) {
transEl.querySelectorAll('p:last-child').forEach(e => e.remove()) // 清理最后一行
s += `
${transEl.innerHTML}
`
}
}
return {text, phonetic, sound, html: s}
},
query(q) {
return new Promise((resolve, reject) => {
// if (q.length > 100) return reject('The text is too large!')
let url = `https://www.youdao.com/w/eng/${encodeURIComponent(q)}`
httpGet(url, 'document').then(r => {
if (r) {
resolve(this.unify(r, q))
} else {
reject('youdao.com error!')
}
}).catch(e => {
reject(e)
})
})
},
link(q) {
return `https://www.youdao.com/w/eng/${encodeURIComponent(q)}`
},
}
}
================================================
FILE: src/js/favorite.js
================================================
'use strict'
/**
* Dream Translate
* https://github.com/ryanker/dream_translate
* @Author Ryan
* @license MIT License
*/
let db, cateId = 0
let sentenceData = {}
let listen, record, compare
document.addEventListener('DOMContentLoaded', async function () {
await idb('favorite', 1, initFavorite).then(r => db = r)
initCate() // 加载分类
createCate() // 添加分类
updateCate() // 编辑分类
deleteCate() // 删除分类
selectAll() // 全选/取消全选
moveSentence() // 批量移动句子
deleteBatchSentence() // 批量删除句子
exportZip() // 导出
importZip() // 导入
openSetting() // 设置
})
// 添加分类
function createCate() {
$('create_cate_but').addEventListener('click', function () {
ddi({
title: '新增分类', body: `
分类名称
`
})
$('create_cate').addEventListener('click', () => {
let cateName = $('create_cateName').value.trim()
if (!cateName) return dal('请填写分类名称', 'error')
let d = new Date().toJSON()
db.create('cate', {cateName, updateDate: d, createDate: d}).then(_ => {
removeDdi()
initCate()
}).catch(e => {
let err = e.target.error.message
let msg = '添加失败'
if (err && err.includes('uniqueness requirements')) msg = '分类已存在,请勿重复添加'
dal(msg, 'error')
})
})
})
}
// 编辑分类
function updateCate() {
$('cate_edit').addEventListener('click', function () {
ddi({
title: '编辑分类', body: `
分类名称
`
})
let updateEl = $('update_cateName')
updateEl.value = $('cate_name').innerText
$('update_cate').addEventListener('click', () => {
let cateName = updateEl.value.trim()
if (!cateName) return dal('分类名称不能为空', 'error')
db.update('cate', cateId, {cateName}).then(_ => {
removeDdi()
initCate()
}).catch(e => {
let err = e.target.error.message
let msg = '修改失败'
if (err && err.includes('uniqueness requirements')) msg = '分类名称不允许重名'
dal(msg, 'error')
})
})
})
}
// 删除分类
function deleteCate() {
$('cate_delete').addEventListener('click', function () {
dco('删除分类不可恢复,确认删除吗?', () => {
if (cateId < 1) return dal('系统分类不允许删除', 'error')
db.count('sentence', 'cateId', cateId).then(n => {
if (n > 0) return dal('分类存在数据,请先清空数据', 'error')
db.delete('cate', cateId).then(_ => initCate(0)).catch(_ => dal('删除失败', 'error'))
})
})
})
}
// 批量移动句子
function moveSentence() {
$('sentence_move').addEventListener('click', function () {
db.getAll('cate').then(arr => {
let s = ''
arr.forEach(v => {
if (v.cateId === cateId) return // 排除当前分类
s += ``
})
ddi({
title: '移动到', body: `
`,
onClose: () => {
listen.stop()
initSentence(cateId)
}
})
// 绑定事件
let tabEl = D('.player_box u[data-type]')
let boxEl = $('skill_box')
tabEl.forEach(e => {
e.addEventListener('click', () => {
// 练习状态时,不允许切换菜单,防止冲突
if (window.isExercising) {
dal('练习状态时,不允许切换菜单', 'error')
return
}
let len = sentenceData.length
let key = Number(el.parentNode.dataset.key)
let type = e.dataset.type
let s = ''
if (type === 'skill') {
s += ''
} else if (type === 'record') {
s += ''
} else if (type === 'listen') {
s += ''
}
s += `
0 次
`
s += ``
if (type === 'listen') {
s += `
播放次数
停止播放
`
}
s += window.playerTips
boxEl.innerHTML = s
rmClassD(tabEl, 'active')
addClass(e, 'active')
playerInit(key, type)
// 下一句
$('next_but').addEventListener('click', function () {
let nextKey = Number(el.parentNode.dataset.key) + 1
let newKey = nextKey >= len ? 0 : nextKey
el.parentNode.dataset.key = String(newKey)
this.querySelector('span').innerText = String(newKey + 1)
$('practice_num').innerText = 0
rmClass($('player_sentence'), 'hide')
playerInit(newKey, type)
})
// 停止播放
if (type === 'listen') {
$('stop_but').addEventListener('click', () => {
listen.stop()
listen.showControls()
})
}
})
})
// 初始
setTimeout(() => {
let el = S('.player_box u[data-type="skill"]')
if (el) el.click()
}, 100)
})
})
}
// 加载播放器
function playerInit(key, type) {
let maxDuration = 5000
let practiceNum = 0
let row = sentenceData[key] || {}
let sentence = row.sentence || ''
let words = row.words || ''
let records = row.records || 0
let days = row.days || 0
let practiceDate = row.practiceDate || ''
let senEl = $('player_sentence')
let nextEl = $('next_but')
// 显示句子
senEl.innerHTML = pointSentence(sentence, words, type === 'record')
// 练习次数
let setPracticeNum = function (n, isUpdate) {
let el = $('practice_num')
if (el) el.innerText = n
// 更新 DB
if (isUpdate) {
records++
if (practiceDate) {
let oldDate = getDate(practiceDate, true).replace(/\D/g, '')
let nowDate = getDate(null, true).replace(/\D/g, '')
if (oldDate < nowDate) days++
} else {
days++
}
practiceDate = new Date().toJSON()
row.records = records
row.days = days
row.practiceDate = practiceDate
db.update('sentence', row.id, {records, days, practiceDate})
}
}
// 加载完成
if (type === 'skill') {
listen = playerListen('player_listen', {
onReady: function (duration) {
let times = 2
if (duration > 10) times *= 2.5 // 时间越长,模仿越难
maxDuration = Math.ceil(duration * times) * 1000
record.setMaxDuration(maxDuration)
}
})
listen.loadBlob(row.blob)
record = playerRecord('player_record', {
showStartBut: true,
maxDuration,
onStart: () => {
window.isExercising = true // 用来限制练习状态时,不允许切换菜单
nextEl.disabled = true
},
onStop: () => {
compare.loadBlob(row.blob)
compare.once('finish', () => {
let t = setTimeout(() => record.showStartBut(), maxDuration + 1000)
setTimeout(() => {
compare.loadBlob(record.blob)
compare.once('finish', () => {
clearTimeout(t)
record.showStartBut()
window.isExercising = false // 解除限制
nextEl.disabled = false // 解除禁用
setPracticeNum(++practiceNum, true) // 练习次数
if (practiceNum === 10) addClass(senEl, 'hide') // 提升难度,隐藏文字
})
}, 100)
})
},
})
compare = playerCompare('player_compare')
} else if (type === 'record') {
listen = playerListen('player_listen', {
onReady: function (duration) {
let times = 2
if (duration > 10) times *= 2.5 // 时间越长,模仿越难
maxDuration = Math.ceil(duration * times) * 1000
record.setMaxDuration(maxDuration)
},
onPlay: () => {
window.isExercising = true // 用来限制练习状态时,不允许切换菜单
nextEl.disabled = true
},
onFinish: () => record.start(), // 开始录音
})
listen.loadBlob(row.blob)
record = playerRecord('player_record', {
maxDuration,
onStop: () => {
compare.loadBlob(row.blob)
compare.once('finish', () => {
let t = setTimeout(() => listen.showControls(), maxDuration + 1000) // 显示开始录音按钮
setTimeout(() => {
compare.loadBlob(record.blob)
compare.once('finish', () => {
clearTimeout(t)
listen.showControls() // 显示播放按钮
window.isExercising = false // 解除限制
nextEl.disabled = false // 解除禁用
setPracticeNum(++practiceNum, true) // 练习次数
if (practiceNum === 5) senEl.innerHTML = pointSentence(sentence, words) // 降低难度,显示文字
if (practiceNum === 10) addClass(senEl, 'hide') // 提升难度,隐藏文字
})
}, 100)
})
}
})
compare = playerCompare('player_compare')
} else if (type === 'listen') {
listen = playerListen('player_listen', {
onFinish: () => {
listen.play()
let nEl = $('player_num')
let n = nEl && nEl.value ? Number(nEl.value) : 2
setPracticeNum(++practiceNum) // 练习次数
if (practiceNum > 10) addClass(senEl, 'hide') // 提升难度,隐藏文字
if (practiceNum >= n) {
$('next_but').click()
setTimeout(() => listen.play(), 100)
}
}
})
listen.loadBlob(row.blob)
}
}
// 解析重点词汇
function pointSentence(sentence, words, isUnderscore) {
let s = HTMLEncode(sentence)
let arr = words.split('\n')
arr = uniqueArray(arr)
for (let v of arr) {
v = HTMLEncode(v.trim())
if (!v) continue
v = v.replace(/([.?+*])/g, '\\$1')
let reg = new RegExp(`^(${v})\\W|\\W(${v})\\W|\\W(${v})$|^(${v})$`, 'g')
// console.log(reg)
s = s.replace(reg, (...args) => {
let str = args[0]
let word = args.slice(1, -2).join('')
if (isUnderscore) {
return str.replace(word, '___')
} else {
if (word === 'u') return str
return str.replace(word, `${word}`)
}
})
}
return s
}
// 修改句子
function editSentence() {
D('.dmx_button[data-action="edit"]').forEach(el => {
el.addEventListener('click', () => {
ddi({
title: '修改',
body: `
句子
生词
备注
`
})
let id = Number(el.parentNode.dataset.id)
let formEl = $('sentence_form')
let sentenceEl = formEl.querySelector('[name="sentence"]')
let wordsEl = formEl.querySelector('[name="words"]')
let remarkEl = formEl.querySelector('[name="remark"]')
let submitEl = formEl.querySelector('button[type="submit"]')
db.read('sentence', id).then(row => {
sentenceEl.value = row.sentence
wordsEl.value = row.words
remarkEl.value = row.remark
})
submitEl.addEventListener('click', () => {
let sentence = sentenceEl.value.trim()
if (!sentence) return dal('句子内容不能为空', 'error')
db.update('sentence', id, {
sentence,
words: wordsEl.value,
remark: remarkEl.value,
}).then(_ => {
removeDdi()
initSentence(cateId)
}).catch(_ => dal('修改失败', 'error'))
})
})
})
}
// 删除句子
function deleteSentence() {
D('.dmx_button[data-action="delete"]').forEach(el => {
el.addEventListener('click', () => {
let id = Number(el.parentNode.dataset.id)
dco(`删除不可恢复,您确认要删除吗?`, () => {
db.delete('sentence', id).then(_ => initSentence(cateId)).catch(_ => dal('删除失败', 'error'))
})
})
})
}
// 显示批量操作按钮
function selectBind() {
let eList = D('td.tb_checkbox input[type="checkbox"]')
eList.forEach(el => {
el.addEventListener('click', () => {
let len = 0
eList.forEach(e => e.checked && len++)
;(len > 0 ? addClass : rmClass)($('extra_but'), 'dmx_show')
})
})
}
// 导出
function exportZip() {
$('export').addEventListener('click', async function () {
loading('打包下载...')
let zip = new JSZip()
// cate
await db.find('cate').then(arr => {
zip.file(`cate.json`, JSON.stringify(arr))
})
// sentence
let sentence = {}
let typeArr = {}
await db.find('sentence').then(arr => {
for (let v of arr) {
// zip.file(`${v.id}.json`, JSON.stringify(v))
zip.file(`mp3/${v.id}.mp3`, v.blob)
typeArr[v.id] = v.blob.type
delete v.blob
}
sentence = arr
})
zip.file(`sentence.json`, JSON.stringify(sentence))
zip.file(`mp3Type.json`, JSON.stringify(typeArr))
debug('zip generateAsync ...')
await zip.generateAsync({type: 'blob'}).then(function (blob) {
downloadZip(blob)
removeDdi()
}).catch(err => console.warn('zip generateAsync error:', err))
})
}
// 下载 ZIP
function downloadZip(blob) {
let el = document.createElement('a')
el.href = URL.createObjectURL(blob)
el.download = `梦想划词翻译-${getDate().replace(/\D/g, '')}.zip`
el.click()
}
// 导入
function importZip() {
$('import').addEventListener('click', function () {
ddi({
title: '导入', body: `
清空数据
初始统计
`
})
let butEl = $('upload_but')
butEl.addEventListener('click', () => {
let inp = document.createElement('input')
inp.type = 'file'
inp.accept = 'application/zip'
inp.onchange = function () {
let files = this.files
if (files.length < 1) return
let f = files[0]
// if (f.type !== 'application/zip') return // windows 系统识别类型为 application/x-zip-compressed,从而导致 bug
if (!f.type.includes('zip')) {
dal('请选择正确的压缩包文件!', 'error')
return
}
butEl.disabled = true
butEl.innerText = '正在导入...'
let tStart = new Date()
let isClear = $('import_clear').checked
let isInitial = $('import_initial').checked
JSZip.loadAsync(f).then(async function (zip) {
// zip.forEach((filename, file) => console.log(filename, file)) // zip 详情
let errStr = ''
let errNum = 0
let errAppend = (e) => {
errNum++
errStr += e + JSON.stringify(e) + '\n'
}
// mp3Type
let mp3TypeObj = {}
try {
let mp3Type = await zip.file('mp3Type.json').async('text')
mp3TypeObj = JSON.parse(mp3Type)
} catch (e) {
errAppend(e)
}
// cate
let cateArr = []
try {
let cate = await zip.file('cate.json').async('text')
cateArr = JSON.parse(cate)
} catch (e) {
errAppend(e)
}
// sentence
let sentenceNum = 0
let sentenceRepeat = 0 // 重复的句子
let sentenceArr = []
try {
let sentence = await zip.file('sentence.json').async('text')
sentenceArr = JSON.parse(sentence)
} catch (e) {
errAppend(e)
}
if (isClear) {
// 清空数据
db.clear('sentence').then(_ => debug('sentence clear finish.')).catch(e => errAppend(e))
db.clear('cate').then(_ => debug('cate clear ok.')).catch(e => errAppend(e))
// cate
for (let v of cateArr) db.create('cate', v).catch(e => errAppend(e))
// sentence
for (let v of sentenceArr) {
await zip.file(`mp3/${v.id}.mp3`).async('blob').then(b => {
v.blob = b.slice(0, b.size, mp3TypeObj[v.id] || 'audio/mpeg') // 设置 blob 类型
})
if (isInitial) {
v.records = 0
v.days = 0
}
await db.create('sentence', v).then(r => {
sentenceNum++
butEl.innerText = `正在导入... ${sentenceNum}/${sentenceArr.length}`
debug('sentence create:', v.id, r)
}).catch(e => errAppend(e))
}
} else {
// cate 对应表
let cateMap = {}
for (let v of cateArr) {
let row = null
await db.readByIndex('cate', 'cateName', v.cateName.trim()).then(r => row = r).catch(e => errAppend(e))
if (!row) {
// 不存在就创建
let oldId = v.cateId
delete v.cateId
await db.create('cate', v).then(r => {
cateMap[oldId] = r.target.result // 对应新创建的ID
}).catch(e => errAppend(e))
} else {
cateMap[v.cateId] = row.cateId // 存在就记录对应的ID
}
}
// sentence
for (let v of sentenceArr) {
// 判断句子是否存在
let sentence = null
await db.readByIndex('sentence', 'sentence', v.sentence.trim()).then(r => sentence = r).catch(e => errAppend(e))
if (sentence) {
sentenceRepeat++
continue // 如果存在,就跳过
}
// 初始统计
if (isInitial) {
v.records = 0
v.days = 0
}
// 获取音频
await zip.file(`mp3/${v.id}.mp3`).async('blob').then(b => {
v.blob = b.slice(0, b.size, mp3TypeObj[v.id] || 'audio/mpeg') // 设置 blob 类型
})
// 写入数据库
v.cateId = cateMap[v.cateId] || 0
delete v.id
await db.create('sentence', v).then(r => {
sentenceNum++
butEl.innerText = `正在导入... ${sentenceNum}/${sentenceArr.length}`
debug('create sentenceId:', r.target.result)
}).catch(e => errAppend(e))
}
}
let okMsg = `导入完成 导入:${sentenceNum} 条`
if (sentenceRepeat > 0) okMsg += `,重复:${sentenceRepeat} 条`
if (errNum > 0) {
okMsg += `,错误:${errNum} 次`
console.warn('errStr:', errStr)
}
okMsg += ` 耗时:${new Date() - tStart} ms`
dal(okMsg, 'success', () => {
// location.reload()
removeDdi()
initCate(cateId)
initSentence(cateId)
})
}).catch(e => {
dal('读取压缩包失败', 'error', () => removeDdi())
debug('loadAsync error:', e)
})
}
inp.click()
})
})
}
// 设置
function openSetting() {
$('setting').addEventListener('click', function () {
ddi({
title: '设置', body: `
展示顺序
`
})
$('order_by').value = localStorage['orderBy'] || 'obverse'
$('save_but').addEventListener('click', () => {
localStorage.setItem('orderBy', $('order_by').value)
removeDdi()
initSentence(cateId)
})
})
}
// 全选/取消全选
function selectAll() {
$('selectAll').addEventListener('click', function () {
let eList = D('td.tb_checkbox input[type="checkbox"]')
eList.forEach(el => el.checked = this.checked)
;(this.checked && eList.length > 0 ? addClass : rmClass)($('extra_but'), 'dmx_show')
})
}
// 取消选中
function selectCancel() {
$('selectAll').checked = false
rmClass($('extra_but'), 'dmx_show')
}
// 随机数组
function shuffle(arr) {
for (let k = 0; k < arr.length; k++) {
let i = Math.floor(Math.random() * arr.length);
[arr[k], arr[i]] = [arr[i], arr[k]]
}
return arr
}
================================================
FILE: src/js/frame.js
================================================
'use strict'
/**
* Dream Translate
* https://github.com/ryanker/dream_translate
* @Author Ryan
* @license MIT License
*/
;(function () {
let frame = window.frameElement
if (frame) {
let top = null
try {
top = window.top || window.parent
} catch (e) {
return
}
if (!top) return
document.addEventListener('mouseup', function (e) {
let bcr = frame.getBoundingClientRect()
let clientX = e.clientX + bcr.left
let clientY = e.clientY + bcr.top
let text = window.getSelection().toString().trim()
if (text) top.postMessage({text: text, clientX: clientX, clientY: clientY}, '*')
})
document.addEventListener('mouseup', function () {
top._MxDialog && top._MxDialog.hide()
})
}
})()
================================================
FILE: src/js/history.js
================================================
'use strict'
/**
* Dream Translate
* https://github.com/ryanker/dream_translate
* @Author Ryan
* @license MIT License
*/
let db
let bg = B.getBackgroundPage()
document.addEventListener('DOMContentLoaded', async function () {
await idb('history', 1, initHistory).then(r => db = r)
historyList() // 展示列表
selectAll() // 全选/取消全选
deleteMultiple() // 批量删除
openSetting() // 设置
})
function historyList() {
let thLen = D('#history_box thead th').length
let tbodyEl = S('#history_box tbody')
tbodyEl.innerHTML = `
`
// 播放
function playerListen(id, options) {
if (!window._playerListen) window._playerListen = []
let p = window._playerListen
if (p[id]) {
p[id].destroy()
}
// 创建元素
let did = document.getElementById(id)
let wid = id + '_waveform'
did.innerHTML = `
Listen
`
// 初始参数
let o = Object.assign({
url: '',
onReady: null,
onPlay: null,
onFinish: null,
}, options)
// 基本元素
let p_current = did.querySelector('.dmx_p_current')
let p_duration = did.querySelector('.dmx_p_duration')
let p_controls = did.querySelector('.dmx_controls')
// 创建播放器
let wsId = document.getElementById(wid)
let height = wsId.clientHeight
let ws, maxDuration
ws = WaveSurfer.create({
container: wsId,
height: height,
barWidth: 3,
barHeight: 2,
backend: 'WebAudio',
backgroundColor: '#66CCCC', // 背景色
waveColor: '#CCFF66', // 波纹色
progressColor: '#FF9900', // 填充色(播放后)
cursorColor: '#666633', // 指针色
hideScrollbar: true,
})
o.url && ws.load(o.url)
ws.hideControls = function () {
p_controls.style.display = 'none'
}
ws.showControls = function () {
p_controls.style.display = 'flex'
}
ws.on('ready', function () {
maxDuration = ws.getDuration()
if (maxDuration > 0) {
p_duration.innerText = ' / ' + humanTime(maxDuration)
p_current.innerText = '00:00:000'
}
typeof o.onReady === 'function' && o.onReady(maxDuration)
})
ws.on('loading', function (percents) {
p_controls.style.display = percents === 100 ? 'flex' : 'none'
})
ws.on('audioprocess', function (duration) {
p_current.innerText = humanTime(duration)
})
ws.on('play', function () {
ws.hideControls()
typeof o.onPlay === 'function' && o.onPlay.call(ws)
})
ws.on('finish', function () {
p_current.innerText = humanTime(maxDuration)
typeof o.onFinish === 'function' ? o.onFinish.call(ws) : ws.showControls()
})
p_controls.addEventListener('click', ws.playPause.bind(ws)) // 绑定事件
window._playerListen[id] = ws
return ws
}
// 录音
function playerRecord(id, options) {
if (!navigator.mediaDevices) return
if (!window._playerRecord) window._playerRecord = []
let p = window._playerRecord
if (p[id]) {
if (p[id].ws) p[id].ws.destroy()
if (p[id].recorder) p[id].recorder.destroy()
}
// 创建元素
let did = document.getElementById(id)
let wid = id + '_waveform'
did.innerHTML = `
Record
`
// 初始参数
let o = Object.assign({
showStartBut: false,
maxDuration: 5 * 1000,
mp3Enable: true, // safari 浏览器才启用
onStart: null,
onStop: null,
}, options)
// 元素
let p_current = did.querySelector('.dmx_p_current')
let p_duration = did.querySelector('.dmx_p_duration')
let p_circle = did.querySelector('.dmx_circle')
let p_start = did.querySelector('.dmx_controls button')
let wsId = document.getElementById(wid)
let height = wsId.clientHeight
// 初始对象
let obj = {
duration: 0,
recordStartTime: 0, // 开始录制时间
recorder: null,
microphone: null,
ws: null,
active: false,
ButEl: {},
blob: null,
}
// 录音中按钮效果
obj.ButEl.start = () => addClass(p_circle, 'dmx_on')
// 录音停止按钮效果
obj.ButEl.stop = () => rmClass(p_circle, 'dmx_on')
// 绑定开始录音事件
p_start.addEventListener('click', function () {
!obj.active && obj.start()
})
// 绑定停止录音事件
p_circle.addEventListener('click', function () {
if (!obj.active) return
// 限制最短录音时长
let minTime = 500
if (!obj.recordStartTime || ((new Date() * 1) - obj.recordStartTime < minTime)) return
obj.stop()
})
obj.showStartBut = function () {
p_start.style.display = 'flex'
p_circle.style.display = 'none'
}
obj.hideStartBut = function () {
p_start.style.display = 'none'
p_circle.style.display = 'flex'
}
// 初始按钮显示
o.showStartBut ? obj.showStartBut() : obj.hideStartBut()
// 定时器
let t, tEnd
let timeOutStart = function () {
obj.recordStartTime = new Date() * 1 // 开始录制时间
tEnd = (new Date() * 1) + Number(o.maxDuration)
t = setInterval(function () {
let remain = tEnd - (new Date() * 1)
if (remain > 0) {
p_current.innerText = humanTime((o.maxDuration - remain) / 1000)
} else {
obj.stop()
clearInterval(t)
p_current.innerText = humanTime(o.maxDuration / 1000)
}
}, 30)
}
let timeOutStop = function () {
if (tEnd < (new Date() * 1)) return
let remain = tEnd - (new Date() * 1)
if (remain > 0) {
p_current.innerText = humanTime((o.maxDuration - remain) / 1000)
clearInterval(t)
}
}
// 设置最大录音时长
obj.setMaxDuration = function (maxDuration) {
o.maxDuration = Number(maxDuration)
}
// 捕获麦克风
obj.captureMicrophone = function (callback) {
navigator.mediaDevices.getUserMedia({audio: true}).then(function (stream) {
obj.microphone = stream
callback(obj.microphone)
})
}
// 停止麦克风
obj.stopMicrophone = function () {
if (!obj.microphone) return
if (obj.microphone.getTracks) {
// console.log('microphone getTracks stop...');
obj.microphone.getTracks().forEach(stream => stream.stop())
} else if (obj.microphone.stop) {
// console.log('microphone stop...');
obj.microphone.stop()
}
obj.microphone = null
}
// 销毁
obj.destroy = function () {
obj.stopMicrophone()
if (obj.recorder) {
obj.recorder.destroy()
obj.recorder = null
}
if (obj.ws) {
obj.ws.destroy()
obj.ws = null
}
}
// 开始录制
obj.start = function () {
if (obj.active) return
obj.active = true
obj.recordStartTime = 0
// 切换按钮显示
if (o.showStartBut) obj.hideStartBut()
// 开始录音回调
typeof o.onStart === 'function' && o.onStart.call(obj)
// 初始时间
p_duration.innerText = ' / ' + humanTime(o.maxDuration / 1000)
p_current.innerText = '00:00:000'
if (obj.recorder) obj.recorder.destroy()
if (obj.ws === null) {
obj.ws = WaveSurfer.create({
container: wsId,
height: height,
barWidth: 3,
barHeight: 2,
cursorColor: '#CED5E2', // 指针色
hideScrollbar: true,
interact: false,
plugins: [WaveSurfer.microphone.create()]
})
obj.ws.microphone.on('deviceReady', function (stream) {
obj.microphone = stream
setTimeout(() => {
let options = isFirefox ? {disableLogs: true} : {type: 'audio', disableLogs: true}
obj.recorder = window.RecordRTC(stream, options)
obj.recorder.startRecording()
timeOutStart() // 定时器
obj.ButEl.start() // 录音中
}, 300)
})
obj.ws.microphone.on('deviceError', function (code) {
console.warn('Device error: ' + code)
})
obj.ws.microphone.start()
} else {
!obj.ws.microphone.active && obj.ws.microphone.start()
}
}
// 停止录音
obj.stop = function () {
if (!obj.active) return
obj.active = false
timeOutStop() // 停止定时器
obj.ButEl.stop() // 停止录音
// 停止录音器波纹
obj.ws.microphone.active && obj.ws.microphone.stop()
// 停止录音
obj.recorder.stopRecording(function () {
// obj.url = this.toURL();
obj.blob = this.getBlob()
typeof o.onStop === 'function' && o.onStop.call(obj) // 停止录音回调
})
}
window._playerRecord[id] = obj
return obj
}
// 对比
function playerCompare(id, options) {
if (!window._playerCompare) window._playerCompare = []
let p = window._playerCompare
if (p[id]) {
p[id].destroy()
}
let did = document.getElementById(id)
let wid = id + '_waveform'
did.innerHTML = `
Compare
`
// 初始参数
let o = Object.assign({
url: '',
autoPlay: true,
}, options)
// 初始化
let p_current = did.querySelector('.dmx_p_current')
let p_duration = did.querySelector('.dmx_p_duration')
let but = did.querySelector('.dmx_circle')
// 创建播放器
let wsId = document.getElementById(wid)
let height = wsId.clientHeight
let ws = WaveSurfer.create({
container: wsId,
height: height,
barWidth: 3,
barHeight: 2,
waveColor: '#FFFF66', // 波纹色
progressColor: '#FFCC99', // 填充色(播放后)
cursorColor: '#333', // 指针色
hideScrollbar: true,
interact: false,
})
o.url && ws.load(o.url)
let maxDuration, isClickPlay
ws.on('ready', function () {
maxDuration = ws.getDuration()
if (maxDuration > 0) {
p_duration.innerText = ' / ' + humanTime(maxDuration)
p_current.innerText = '00:00:000'
}
ws.setBackgroundColor('#66b1ff')
// 自动播放
if (o.autoPlay) {
isClickPlay = true
ws.play()
}
})
ws.on('audioprocess', function (duration) {
p_current.innerText = humanTime(duration)
})
ws.on('play', function () {
addClass(but, 'dmx_on')
})
ws.on('finish', function () {
isClickPlay = false
p_current.innerText = humanTime(maxDuration)
ws.setBackgroundColor('')
ws.empty()
rmClass(but, 'dmx_on')
})
window._playerCompare[id] = ws
// 解决 Safari 浏览器自动播放音频失败问题
// but.addEventListener('click', () => {
// isClickPlay && ws.play()
// })
return ws
}
function humanTime(s, isSecond) {
if (s <= 0) return isSecond ? '00:00:00' : '00:00:000'
let hs = Math.floor(s / 3600)
let ms = hs > 0 ? Math.floor((s - hs * 3600) / 60) : Math.floor(s / 60)
if (isSecond) {
return zero(hs) + ':' + zero(ms) + ':' + zero(Math.floor(s % 60))
} else {
let se = (s % 60).toFixed(3).replace('.', ':')
if (hs > 0) {
return zero(hs) + ':' + zero(ms) + ':' + zero(se, 6)
} else {
return zero(ms) + ':' + zero(se, 6)
}
}
}
================================================
FILE: src/js/popup.js
================================================
'use strict'
/**
* Dream Translate
* https://github.com/ryanker/dream_translate
* @Author Ryan
* @license MIT License
*/
window.isPopup = true
================================================
FILE: src/js/record.js
================================================
'use strict'
/**
* Dream Translate
* https://github.com/ryanker/dream_translate
* @Author Ryan
* @license MIT License
*/
let bg = B.getBackgroundPage()
let audioSrc = bg.audioSrc || {}
let maxDuration = 5000
let practiceNum = 0
let listen = {}, listen2 = {}, record, compare
document.addEventListener('DOMContentLoaded', function () {
playerInit()
// 加载音频
setTimeout(() => {
if (audioSrc.blob) {
listen.loadBlob(audioSrc.blob)
} else if (audioSrc.url) {
bg.getAudioBlob(audioSrc.url).then(b => {
listen.loadBlob(b)
audioSrc.blob = b
})
}
}, 200)
let record_box = $('record_box')
let favorite_form = $('favorite_form')
let favorite_but = $('favorite_but')
let sentence_form = $('sentence_form')
let back_but = $('back_but')
let sentenceInp = S('input[name="sentence"]')
let urlInp = S('input[name="url"]')
let wordsTex = S('textarea[name="words"]')
// 练习提示
record_box.insertAdjacentHTML('beforeend', window.playerTips)
// 添加收藏
favorite_but.addEventListener('click', () => {
addClass(record_box, 'dmx_hide')
addClass(favorite_form, 'dmx_show')
if (bg.textTmp) sentenceInp.value = bg.textTmp
let {url, blob} = audioSrc
listen2 = playerListen('player_listen2')
if (blob) listen2.loadBlob(blob)
if (url) urlInp.value = url
})
// 修改链接
urlInp.addEventListener('blur', () => {
let url = urlInp.value.trim()
if (url && url !== audioSrc.url) bg.getAudioBlob(url).then(blob => listen2.loadBlob(blob))
})
// 返回
back_but.addEventListener('click', () => {
rmClass(record_box, 'dmx_hide')
rmClass(favorite_form, 'dmx_show')
})
// 提交表单
sentence_form.addEventListener('submit', (e) => {
e.preventDefault()
idb('favorite', 1, initFavorite).then(async db => {
// 如果链接修改过,重新获取二进制文件
let url = urlInp.value.trim()
if (url && url !== audioSrc.url) await bg.getAudioBlob(url).then(b => audioSrc.blob = b)
await db.create('sentence', {
cateId: 0,
sentence: sentenceInp.value.trim(),
words: wordsTex.value.trim(),
remark: '',
records: 0,
days: 0,
url,
blob: audioSrc.blob,
practiceDate: '',
createDate: new Date().toJSON(),
}).then(() => {
dal('添加完成', 'success', () => {
sentenceInp.value = ''
wordsTex.value = ''
back_but.click()
})
}).catch(e => {
// console.log(e)
let err = e.target.error.message
let msg = '添加失败'
if (err && err.includes('uniqueness requirements')) msg = '句子已存在,请勿重复添加'
dal(msg, 'error')
})
})
})
})
// 重新渲染
window.addEventListener('resize', function (e) {
_setTimeout('resize', () => {
if (audioSrc.blob) listen.loadBlob(audioSrc.blob)
}, 1000)
})
function playerInit() {
listen = playerListen('player_listen', {
onReady: function (duration) {
let times = 2
if (duration > 10) times *= 2.5 // 时间越长,模仿越难
maxDuration = Math.ceil(duration * times) * 1000
record.setMaxDuration(maxDuration)
},
onFinish: () => {
record.start() // 开始录音
},
})
record = playerRecord('player_record', {
maxDuration,
onStop: () => {
compare.loadBlob(audioSrc.blob)
compare.once('finish', () => {
let t = setTimeout(() => listen.showControls(), maxDuration + 1000) // 显示播放按钮
setTimeout(() => {
// compare.load(URL.createObjectURL(record.blob))
compare.loadBlob(record.blob)
compare.once('finish', () => {
clearTimeout(t)
listen.showControls() // 显示播放按钮
$('practice_num').innerText = ++practiceNum // 练习次数
})
}, 100)
})
},
})
compare = playerCompare('player_compare')
}
================================================
FILE: src/js/setting.js
================================================
'use strict'
/**
* Dream Translate
* https://github.com/ryanker/dream_translate
* @Author Ryan
* @license MIT License
*/
let conf, setting
let searchText, searchList
document.addEventListener('DOMContentLoaded', async function () {
await fetch('../conf/conf.json').then(r => r.json()).then(r => {
conf = r
})
await storageSyncGet(['setting', 'searchText']).then(function (r) {
setting = r.setting
searchText = r.searchText || ''
})
await fetch('../conf/searchText.txt').then(r => r.text()).then(r => {
searchText = searchText || r.trim()
searchList = getSearchKey(searchText)
})
init()
// debug('conf:', conf)
// debug('setting:', setting)
})
function init() {
// 词典发音列表
let dictionarySoundList = {}
for (let [k, v] of Object.entries(conf.dictionaryList)) {
if (conf.dictionarySoundExcluded.includes(k)) continue // 排除
dictionarySoundList[k] = v
}
// 绑定导航
navigate('navigate', '.setting_box')
// 初始参数
settingBoxHTML('setting_translate_list', 'translateList', conf.translateList)
settingBoxHTML('setting_translate_tts_list', 'translateTTSList', conf.translateTTSList)
settingBoxHTML('setting_dictionary_list', 'dictionaryList', conf.dictionaryList)
settingBoxHTML('setting_dictionary_sound_list', 'dictionarySoundList', dictionarySoundList)
// 初始可替换的本机朗读参数
if (isFirefox) {
$('local_box').style.display = 'none'
} else {
initLocalSoundReplace()
}
// 设置值 & 绑定事件
setBindValue('scribble', setting.scribble)
setBindValue('excludeChinese', setting.excludeChinese)
setBindValue('excludeSymbol', setting.excludeSymbol)
setBindValue('excludeNumber', setting.excludeNumber)
setBindValue('position', setting.position)
setBindValue('allowSelect', setting.allowSelect)
setBindValue('autoCopy', setting.autoCopy)
setBindValue('autoPaste', setting.autoPaste)
setBindValue('autoChange', setting.autoChange)
setBindValue('autoWords', setting.autoWords)
setBindValue('cutHumpName', setting.cutHumpName)
setBindValue('translateList', setting.translateList)
setBindValue('translateTTSList', setting.translateTTSList)
setBindValue('localSoundReplace', setting.localSoundReplace)
setBindValue('translateOCR', setting.translateOCR || 'CHN_ENG')
setBindValue('ocrType', setting.ocrType)
setBindValue('translateThin', setting.translateThin)
setBindValue('hideOriginal', setting.hideOriginal)
setBindValue('autoLanguage', setting.autoLanguage)
setBindValue('autoConfirm', setting.autoConfirm)
setBindValue('dictionaryList', setting.dictionaryList)
setBindValue('dictionarySoundList', setting.dictionarySoundList)
setBindValue('dictionaryReader', setting.dictionaryReader)
// 绑定顺序展示
bindSortHTML('展示顺序:', 'setting_translate_sort', 'translateList', setting.translateList, conf.translateList)
bindSortHTML('朗读顺序:', 'setting_translate_tts_sort', 'translateTTSList', setting.translateTTSList, conf.translateTTSList)
bindSortHTML('展示顺序:', 'setting_dictionary_sort', 'dictionaryList', setting.dictionaryList, conf.dictionaryList)
bindSortHTML('朗读顺序:', 'setting_dictionary_sound_sort', 'dictionarySoundList', setting.dictionarySoundList, dictionarySoundList)
// 搜索设置功能
initSearch()
// 绑定是否显示"朗读"参数
bindShow('setting_dictionary_reader', 'dictionarySoundList', setting.dictionarySoundList)
// 本机 TTS 设置
localTtsSetting()
searchListSetting()
// 文字识别设置
settingOcr()
// 重置设置
$('clearSetting').addEventListener('click', clearSetting)
}
function initSearch() {
settingBoxHTML('setting_search_list', 'searchList', searchList)
settingBoxHTML('setting_search_menus', 'searchMenus', searchList)
settingBoxHTML('setting_search_side', 'searchSide', searchList)
setBindValue('searchList', setting.searchList)
setBindValue('searchMenus', setting.searchMenus)
setBindValue('searchSide', setting.searchSide)
bindSortHTML('展示顺序:', 'setting_search_sort', 'searchList', setting.searchList, searchList)
bindSortHTML('展示顺序:', 'setting_search_menus_sort', 'searchMenus', setting.searchMenus, searchList)
bindSortHTML('展示顺序:', 'setting_search_side_sort', 'searchSide', setting.searchSide, searchList)
// 绑定右键菜单设置
bindSearchMenus()
}
function initLocalSoundReplace() {
let s = ''
let list = conf.translateList
Object.keys(list).forEach(k => {
s += ``
})
N('localSoundReplace')[0].innerHTML = s
}
function getSearchKey(s) {
let r = {}
Object.keys(getSearchList(s)).forEach(k => r[k] = k)
return r
}
function navigate(navId, contentSel) {
let nav = $(navId)
let el = nav.querySelectorAll('u')
let conEl = document.querySelectorAll(contentSel)
el.forEach(fn => {
fn.addEventListener('click', function () {
// 修改活动样式
el.forEach(elu => {
rmClass(elu, 'active')
})
addClass(this, 'active')
// 显示对应框
conEl.forEach(elc => {
elc.style.display = 'none'
})
let target = this.getAttribute('target')
$(target).style.display = 'block'
})
})
nav.querySelector('u.active').click() // 激活初始值
}
function setBindValue(name, value) {
setValue(name, value)
bindValue(name, value)
}
function setValue(name, value) {
let isArr = isArray(value)
let el = N(name)
el && el.forEach(v => {
let nodeName = v.nodeName
if (nodeName === 'SELECT') {
v.value = value
} else if (nodeName === 'INPUT') {
if (isArr) {
let checked = false
for (let val of value) {
if (v.value === val) {
checked = true
break
}
}
v.checked = checked
} else {
if (v.value === value) v.checked = true
}
}
})
}
function bindValue(name, value) {
let isArr = isArray(value)
let el = N(name)
el && el.forEach(v => {
v.addEventListener('change', function () {
let val = this.value
let nodeName = this.nodeName
if (nodeName === 'SELECT') {
value = val
} else if (nodeName === 'INPUT') {
if (isArr) {
if (this.checked) {
value.push(val)
} else {
for (let k in value) {
if (value.hasOwnProperty(k) && val === value[k]) {
value.splice(k, 1)
break
}
}
}
} else {
value = this.checked ? val : ''
}
}
// 保存设置
setSetting(name, value)
})
})
}
function bindSearchMenus() {
N('searchMenus').forEach(v => {
v.addEventListener('change', function () {
// firefox 在 iframe 下功能缺失,只能通过 message 处理
sendMessage({action: 'menu', name: this.value, isAdd: this.checked})
})
})
}
function bindSortHTML(textName, id, name, value, list) {
sortShow(textName, id, value, list) // 初始值
let el = N(name)
el && el.forEach(v => {
v.addEventListener('change', function () {
sortShow(textName, id, value, list)
})
})
}
function sortShow(textName, id, value, list) {
let s = ''
if (isArray(value) && value.length > 0) {
s = textName
value.forEach((v, k) => {
s += (k > 0 ? ' > ' : '') + list[v]
})
}
$(id).innerHTML = s
}
function bindShow(id, name, value) {
let el = N(name)
el && el.forEach(v => {
v.addEventListener('change', function () {
$(id).style.display = (!value || value.length === 0) ? 'none' : 'block'
})
})
$(id).style.display = (!value || value.length === 0) ? 'none' : 'block'
}
function settingBoxHTML(id, name, list) {
let s = ''
Object.keys(list).forEach(v => {
s += ``
})
let el = $(id)
el.innerHTML = s
}
function settingOcr() {
let boxEl = $('baidu_ocr_box')
let akEl = S('input[name="baidu_orc_ak"]')
let skEl = S('input[name="baidu_orc_sk"]')
let clearFn = () => localStorage['clearOcrExpires'] = 'true'
let el = N('ocrType')
el && el.forEach(v => {
v.addEventListener('change', function () {
(this.value === 'baidu' ? addClass : rmClass)(boxEl, 'dmx_show')
clearFn()
})
})
if (setting.ocrType === 'baidu') addClass(boxEl, 'dmx_show')
akEl.value = setting.baidu_orc_ak || ''
skEl.value = setting.baidu_orc_sk || ''
akEl.onblur = () => {
setSetting('baidu_orc_ak', akEl.value)
clearFn()
}
skEl.onblur = () => {
setSetting('baidu_orc_sk', skEl.value)
clearFn()
}
}
function searchListSetting() {
let dialogEl = $('search_list_dialog')
let butEl = $('search_setting_but')
let saveEl = $('search_list_save')
let textEl = S('textarea[name="search_text"]')
butEl.onclick = () => {
dialogEl.style.display = 'block'
addClass(document.body, 'dmx_overflow_hidden')
textEl.value = searchText
}
saveEl.onclick = () => {
searchText = textEl.value.trim()
searchList = getSearchKey(searchText)
// 清理不存在的设置
let keyArr = Object.keys(searchList)
let funNewArr = function (arr, isMenu) {
let newArr = []
arr.forEach(v => {
if (keyArr.includes(v)) {
newArr.push(v)
} else if (isMenu) {
// 移除右键设置
sendMessage({action: 'menu', name: v, isAdd: false})
}
})
return newArr
}
setting.searchList = funNewArr(setting.searchList)
setting.searchMenus = funNewArr(setting.searchMenus)
setting.searchSide = funNewArr(setting.searchSide)
setSetting('searchList', setting.searchList)
setSetting('searchMenus', setting.searchMenus)
setSetting('searchSide', setting.searchSide)
// 重新初始化
initSearch()
sendMessage({action: 'onSaveSearchText', searchText})
dal('保存成功')
}
// 关闭设置
$('search_list_back').onclick = function () {
dialogEl.style.display = 'none'
rmClass(document.body, 'dmx_overflow_hidden')
}
}
function localTtsSetting() {
let listEl = $('local_tts_list')
let dialogEl = $('local_tts_dialog')
let butEl = S('[name="translateTTSList"][value="local"]')
if (isFirefox) {
butEl.parentElement.style.display = 'none'
return
}
// 关闭设置
dialogEl.querySelector('.dialog_back').onclick = function () {
dialogEl.style.display = 'none'
rmClass(document.body, 'dmx_overflow_hidden')
}
// 打开设置
let i = document.createElement('i')
i.className = 'dmx-icon dmx-icon-setting'
i.title = '本机朗读设置'
i.onclick = function (e) {
e.preventDefault()
dialogEl.style.display = 'block'
addClass(document.body, 'dmx_overflow_hidden')
}
butEl.parentNode.appendChild(i)
// 初始设置
let langList = {}, voices = {}
;(async () => {
// 语音包
await fetch('../conf/langSpeak.json').then(r => r.json()).then(r => {
langList = r
})
// 获取发音列表
await getVoices().then(r => {
voices = r
})
// 归类发音列表
let specialLang = ['en', 'es', 'nl']
let voiceList = voiceListSort(voices, specialLang)
// 创建发音列表
let s1 = '', s2 = ''
let ttsKeys = Object.values(conf.ttsList)
for (const [key, val] of Object.entries(voiceList)) {
let preName = langList[key] ? langList[key].zhName : key
let select = `'
let row = `
`)
// 初始发音设置
if (!setting.ttsConf) setting.ttsConf = {}
for (let [k, v] of Object.entries(setting.ttsConf)) {
let vEl = dialogEl.querySelector(`select[key="${k}"]`)
if (vEl) vEl.value = v
}
// 修改发音设置
let sEl = dialogEl.querySelectorAll('select')
sEl.forEach(fn => {
fn.onchange = function () {
let key = fn.getAttribute('key')
setting.ttsConf[key] = this.value
setSetting('ttsConf', setting.ttsConf) // 保存设置
}
})
// 重置发音设置
$('local_tts_reset_setting').onclick = function () {
setSetting('ttsConf', {})
sEl.forEach(fn => {
fn.value = ''
})
}
})()
}
function voiceListSort(voices, specialLang) {
let kArr = Object.keys(voices)
kArr = kArr.sort()
let r = {}
kArr.forEach(k => {
let v = voices[k]
for (let i = 0; i < specialLang.length; i++) {
let lan = specialLang[i]
if (k === lan || (new RegExp(`^${lan}-`)).test(k)) {
if (!r[lan]) r[lan] = []
v.forEach(val => r[lan].push(val))
return
}
}
r[k] = v
})
return r
}
function setSetting(name, value) {
setting[name] = value
sendSetting(setting, name === 'scribble')
}
function clearSetting() {
sendSetting({}, true, true)
setTimeout(() => {
let url = new URL(location.href)
url.searchParams.set('r', Date.now() + '')
location.href = url.toString()
}, 300)
}
function sendSetting(setting, updateIcon, resetDialog) {
if (B.getBackgroundPage) {
B.getBackgroundPage().saveSettingAll(setting, updateIcon, resetDialog)
} else {
// firefox 在 iframe 下功能缺失,所以通过 message 处理
sendMessage({action: 'saveSetting', setting, updateIcon, resetDialog})
}
}
================================================
FILE: src/js/speak.js
================================================
'use strict'
/**
* Dream Translate
* https://github.com/ryanker/dream_translate
* @Author Ryan
* @license MIT License
*/
let langList, voices, conf = {}
let voiceEl = $('speak_voice')
let rateEl = $('speak_rate')
let pitchEl = $('speak_pitch')
let inputEl = $('speak_input')
let buttonEl = $('speak_button')
document.addEventListener('DOMContentLoaded', async function () {
if (isFirefox) {
let d = document.createElement('div')
d.textContent = 'Firefox 不支持本地朗读功能'
d.setAttribute('style', 'padding:5px;text-align:center;color:red;font-weight:bold;font-size:20px')
document.body.appendChild(d)
return
}
// 语音包
await fetch('../conf/langSpeak.json').then(r => r.json()).then(r => {
langList = r
})
// 获取发音列表
await getVoices().then(r => {
voices = r
})
// 添加发音列表
let voiceList = voiceListSort(voices)
for (const [key, val] of Object.entries(voiceList)) {
val.forEach(v => {
let op = document.createElement('option')
op.value = v.voiceName
op.innerText = `${langList[key] ? langList[key].zhName : key} | ${v.voiceName}${v.remote ? ' | 远程' : ''}`
voiceEl.appendChild(op)
})
}
// 初始设置
loadConf()
// 修改设置
voiceEl.addEventListener('change', function () {
setConf('voiceName', this.value)
})
rateEl.addEventListener('change', function () {
setConf('rate', this.value)
})
pitchEl.addEventListener('change', function () {
setConf('pitch', this.value)
})
// 粘贴事件
inputEl.addEventListener('paste', function (e) {
e.stopPropagation()
e.preventDefault()
this.innerText = (e.clipboardData || window.clipboardData).getData('Text')
})
// 开始朗读
buttonEl.addEventListener('click', function () {
let text = inputEl.innerText
let voiceName = voiceEl.value
let rate = rateEl.value
let pitch = pitchEl.value
let options = {}
if (voiceName) options.voiceName = voiceName
if (rate) options.rate = Number(rate)
if (pitch) options.pitch = Number(pitch)
speak(text, options)
})
// 停止朗读
document.addEventListener('keyup', function (e) {
if (e.key === 'Escape') B.tts.stop()
})
})
function voiceListSort(list) {
if (!list) return {}
let kArr = Object.keys(list)
// console.log(kArr.length)
// console.log(JSON.stringify(kArr.sort()))
kArr = kArr.sort() // 排序
let r = {}
if (list['zh-CN']) r['zh-CN'] = list['zh-CN'] // 中文简体放最前面
kArr.forEach(k => {
if (k === 'zh-CN') return
r[k] = list[k]
})
return r
}
function speak(text, options) {
// console.log(text, options)
if (text) {
let arr = B.getBackgroundPage().sliceStr(text, 128)
arr.forEach((v, k) => {
if (k === 0) {
B.tts.speak(v, options)
} else {
B.tts.speak(v, Object.assign({enqueue: true}, options))
}
})
}
}
function setConf(key, value) {
conf[key] = value
localStorage.setItem('speakConf', JSON.stringify(conf))
}
function loadConf() {
let s = localStorage.getItem('speakConf')
if (!s) return
conf = JSON.parse(s)
if (conf.voiceName) voiceEl.value = conf.voiceName
if (conf.rate) rateEl.value = conf.rate
if (conf.pitch) pitchEl.value = conf.pitch
}
================================================
FILE: src/js/translate/alibaba.js
================================================
'use strict'
/**
* Dream Translate
* https://github.com/ryanker/dream_translate
* @Author Ryan
* @license MIT License
*/
function alibabaTranslate() {
return {
langMap: {
"auto": "auto",
"en": "en",
"zh": "zh",
"ru": "ru",
"tr": "tr",
"pt": "pt",
"th": "th",
"id": "id",
"it": "it",
"spa": "es",
"fra": "fr",
"ara": "ar",
"vie": "vi"
},
langMapInvert: {},
pairMap: {
"auto": ["en"],
"en": ["zh", "ru", "es", "fr", "ar", "tr", "pt", "th", "id", "vi"],
"zh": ["en", "vi"],
"ru": ["en", "es", "tr", "it", "fr", "pt"],
"es": ["en", "ru", "tr", "it", "fr", "pt"],
"fr": ["en", "ru", "tr", "it", "es", "pt"],
"ar": ["en", "zh"],
"tr": ["en", "ru", "fr", "it", "es", "pt"],
"pt": ["en", "ru", "fr", "it", "es", "tr"],
"it": ["en", "ru", "fr", "pt", "es", "tr"],
"th": ["en", "zh"],
"id": ["en", "zh"],
"vi": ["en", "zh"]
},
init() {
this.langMapInvert = invertObject(this.langMap)
return this
},
addListenerRequest() {
onBeforeSendHeadersAddListener(this.onChangeHeaders,
{urls: ['*://translate.alibaba.com/*'], types: ['xmlhttprequest']})
},
removeListenerRequest() {
onBeforeSendHeadersRemoveListener(this.onChangeHeaders)
},
onChangeHeaders(details) {
let s = `origin: https://translate.alibaba.com
referer: https://translate.alibaba.com/
sec-fetch-site: same-origin`
return {requestHeaders: details.requestHeaders.concat(requestHeadersFormat(s))}
},
trans(q, srcLan, tarLan) {
srcLan = this.langMap[srcLan] || 'auto'
tarLan = this.langMap[tarLan] || 'zh'
if (!inArray(tarLan, this.pairMap[srcLan])) tarLan = this.pairMap[srcLan][0]
return new Promise((resolve, reject) => {
if (q.length > 5000) return reject('The text is too large!')
this.addListenerRequest()
let url = `https://translate.alibaba.com/translationopenseviceapp/trans/TranslateTextAddAlignment.do`
let p = new URLSearchParams(`srcLanguage=${srcLan}&tgtLanguage=${tarLan}&srcText=${q}&viewType=&source=&bizType=message`)
httpPost({url: url, body: p.toString()}).then(r => {
this.removeListenerRequest()
if (r) {
resolve(this.unify(r, q, srcLan, tarLan))
} else {
reject('alibaba trans error!')
}
}).catch(e => {
this.removeListenerRequest()
reject(e)
})
})
},
unify(r, q, srcLan, tarLan) {
// console.log('alibaba:', r, q, srcLan, tarLan)
if (srcLan === 'auto' && r.recognizeLanguage) srcLan = r.recognizeLanguage
let map = this.langMapInvert
srcLan = map[srcLan] || 'auto'
tarLan = map[tarLan] || ''
let ret = {text: q, srcLan: srcLan, tarLan: tarLan, lanTTS: null, data: []}
let srcArr = q.split('\n')
let tarArr = []
let arr = r && r.listTargetText
arr && arr.forEach(v => {
tarArr = Object.assign(tarArr, v.split('\n'))
})
tarArr.forEach((v, k) => {
ret.data.push({srcText: srcArr[k] || '', tarText: v})
})
return ret
},
async query(q, srcLan, tarLan) {
return checkRetry(() => this.trans(q, srcLan, tarLan))
},
tts(q, lan) {
lan = this.langMap[lan] || 'en'
return new Promise((resolve) => {
// 阿里云 TTS 有点慢,发音效果也不是太理想,懒得解密了,偷懒直接用搜狗的。
let getUrl = (s) => {
return `https://fanyi.sogou.com/reventondc/synthesis?text=${encodeURIComponent(s)}&speed=1&lang=${lan}&from=translateweb&speaker=3`
}
let r = []
let arr = sliceStr(q, 128)
arr.forEach(text => {
r.push(getUrl(text))
})
resolve(r)
})
},
link(q, srcLan, tarLan) {
return `https://translate.alibaba.com/?d_sl=${srcLan}&d_tl=${tarLan}&d_text=${encodeURIComponent(q)}`
},
}
}
================================================
FILE: src/js/translate/baidu.js
================================================
'use strict'
/**
* Dream Translate
* https://github.com/ryanker/dream_translate
* @Author Ryan
* @license MIT License
*/
function baiduTranslate() {
return {
token: {
gtk: '',
token: '',
date: 0,
},
lanTTS: ["en", "zh", "yue", "ara", "kor", "jp", "th", "pt", "spa", "fra", "ru", "de"],
sign(t, e) {
let ye = function (t, e) {
for (let r = 0; r < e.length - 2; r += 3) {
let n = e.charAt(r + 2)
n = n >= "a" ? n.charCodeAt(0) - 87 : Number(n)
n = "+" === e.charAt(r + 1) ? t >>> n : t << n
t = "+" === e.charAt(r) ? t + n & 4294967295 : t ^ n
}
return t
}
let he = '', r = t.length
r > 30 && (t = "" + t.substr(0, 10) + t.substr(Math.floor(r / 2) - 5, 10) + t.substr(-10, 10))
let n = ('' !== he ? he : (he = e || "") || "").split("."), o = Number(n[0]) || 0, a = Number(n[1]) || 0
let c = [], i = 0, u = 0
for (; u < t.length; u++) {
let s = t.charCodeAt(u)
128 > s ? c[i++] = s : (2048 > s ? c[i++] = s >> 6 | 192 : (55296 === (64512 & s) && u + 1 < t.length && 56320 === (64512 & t.charCodeAt(u + 1)) ?
(s = 65536 + ((1023 & s) << 10) + (1023 & t.charCodeAt(++u)), c[i++] = s >> 18 | 240, c[i++] = s >> 12 & 63 | 128) :
c[i++] = s >> 12 | 224, c[i++] = s >> 6 & 63 | 128), c[i++] = 63 & s | 128)
}
let f = o, l = 0
for (; l < c.length; l++) f = ye(f += c[l], "+-a^+6")
return f = ye(f, "+-3^+b+-f"), 0 > (f ^= a) && (f = 2147483648 + (2147483647 & f)), (f %= 1e6).toString() + "." + (f ^ o)
},
init() {
let str = localStorage.getItem('baiduToken')
if (str) this.token = JSON.parse(str)
return this
},
setToken(options) {
this.token = Object.assign(this.token, options)
localStorage.setItem('baiduToken', JSON.stringify(this.token))
},
getToken() {
return new Promise((resolve, reject) => {
httpGet('https://fanyi.baidu.com/').then(r => {
let arr = r.match(/window\.gtk\s=\s['"]([^'"]+)['"];/)
let tArr = r.match(/token:\s'([^']+)'/)
if (!arr) return reject('baidu gtk empty!')
if (!tArr) return reject('baidu token empty!')
let token = {gtk: arr[1], token: tArr[1], date: Math.floor(Date.now() / 36e5)}
this.setToken(token)
resolve(token)
}).catch(e => {
reject(e)
})
})
},
trans(q, srcLan, tarLan) {
return new Promise((resolve, reject) => {
if (q.length > 5000) return reject('The text is too large!')
if (!this.token.gtk) return reject('baidu gtk empty!')
if (!this.token.token) return reject('baidu token empty!')
let sign = this.sign(q, this.token.gtk)
let token = this.token.token
let p = new URLSearchParams(`from=${srcLan}&to=${tarLan}&query=${q}&simple_means_flag=3&sign=${sign}&token=${token}&domain=common`)
httpPost({
url: `https://fanyi.baidu.com/v2transapi?from=${srcLan}&to=${tarLan}`,
body: p.toString()
}).then(r => {
if (r) {
resolve(this.unify(r, q, srcLan, tarLan))
} else {
reject('baidu translate error!')
}
}).catch(e => {
reject(e)
})
})
},
unify(r, text, srcLan, tarLan) {
// console.log('baidu:', r, text, srcLan, tarLan)
// console.log(JSON.stringify(r))
let res = getJSONValue(r, 'trans_result', {})
let data = []
if (res.data) {
res.data.forEach(v => {
if (v.src && v.dst) data.push({srcText: v.src, tarText: v.dst})
})
}
if (setting.translateThin) return {text, srcLan, tarLan, lanTTS: this.lanTTS, data} // 精简显示
// 重点词汇
let s = ''
if (res.keywords && res.keywords.length > 0) {
s += `
重点词汇
`
s += `
`
res.keywords.forEach(v => {
if (v.word && v.means) s += `
${v.word}${v.means.join(';')}
`
})
s += `
`
}
// 百度支持牛津,格林斯,英英等,如果全显示,会很复杂,小框显示也会很乱,所以只显示最简单的部分即可。
// 在翻译领域,除了国际巨头谷歌,在国内做的最好的非百度莫属,然后是搜狗,有道;如今搜狗被腾讯收购,或许未来会改名。-- 2021.1.6
let simple_means = getJSONValue(r, 'dict_result.simple_means')
if (simple_means) {
s += `
`
let {word_name, symbols, word_means, exchange, memory_skill, tags} = simple_means
if (word_name) s += `
${word_name}
` // 查询的单词
let getIconHTML = function (type, text, title) {
let lan = type === 'uk' ? 'uk' : 'en'
let src = `https://fanyi.baidu.com/gettts?lan=${lan}&text=${encodeURIComponent(text)}&spd=3&source=web`
return ``
}
let hasParts = false
if (symbols) {
symbols.forEach(sym => {
// 音标
let {ph_en, ph_am, parts} = sym
if (ph_en || ph_am) {
s += `
`
s += `[${ph_en}${ph_am && ph_en !== ph_am ? ' $ ' + ph_am : ''}]`
s += getIconHTML('uk', text, '英音')
s += getIconHTML('us', text, '美音')
s += `
`
}
// 释义
if (parts && parts.length > 0) {
hasParts = true
s += `
`
parts.forEach(v => {
let {part, means} = v
let firstVal = getJSONValue(means, '0')
if (firstVal && isString(firstVal)) {
s += `
${part ? `${part}` : ''}${means.join(';')}
`
} else {
let firstVal = getJSONValue(means, '0.text')
if (firstVal && isString(firstVal)) {
for (let mv of means) {
let {text, part, means} = mv
s += `
`
// 单词形态
if (exchange) {
let exchangeObj = {
word_third: '第三人称单数',
word_pl: '复数',
word_ing: '现在分词',
word_past: '过去式',
word_done: '过去分词',
word_er: '比较级',
word_est: '最高级',
word_proto: '原型',
}
s += `
`
for (let [k, v] of Object.entries(exchange)) {
if (!v) continue
let wordStr = ''
v.forEach(word => {
if (word) wordStr += `${word}`
})
s += `${exchangeObj[k] || '其他'}${wordStr}`
}
s += `
`
}
// 记忆技巧
if (memory_skill) {
s += `
记忆技巧:${memory_skill}
`
}
// 单词标签
if (tags) {
s += `
`
for (let [k, v] of Object.entries(tags)) {
let tagStr = ''
v.forEach(tag => {
if (tag) tagStr += `${tag}`
})
s += tagStr
}
s += `
`
}
s += `
`
}
// 视频显示,如果有的话。
let videoObj = getJSONValue(r, 'dict_result.queryExplainVideo')
if (videoObj && videoObj.thumbUrl && videoObj.videoUrl) {
// s += ``
let src = B.root + 'html/video.html?' + new URLSearchParams(`thumbUrl=${videoObj.thumbUrl}&videoUrl=${videoObj.videoUrl}`)
s += ``
}
return {text, srcLan, tarLan, lanTTS: this.lanTTS, data, extra: s}
},
async query(q, srcLan, tarLan, noCache) {
if (srcLan === 'auto') {
srcLan = 'en' // 默认值
await httpPost({
url: `https://fanyi.baidu.com/langdetect`,
body: `query=${encodeURIComponent(q)}`
}).then(r => {
if (r.lan) srcLan = r.lan
}).catch(err => {
debug(err)
})
}
if (srcLan === tarLan) tarLan = srcLan === 'zh' ? 'en' : 'zh'
return checkRetry(async (i) => {
let t = Math.floor(Date.now() / 36e5)
let d = this.token.date
if (i > 0) noCache = true
if (noCache || !d || Number(d) !== t) {
await this.getToken().catch(err => {
debug(err)
})
}
return this.trans(q, srcLan, tarLan)
})
},
tts(q, lan) {
return new Promise((resolve, reject) => {
if (!inArray(lan, this.lanTTS)) return reject('This language is not supported!')
if (lan === 'yue') lan = 'cte' // 粤语
// https://tts.baidu.com/text2audio?tex=%E6%98%8E(ming2)%E7%99%BD(bai2)&cuid=baike&lan=ZH&ctp=1&pdt=31&vol=9&spd=4&per=4100
let getUrl = (s) => {
return `https://fanyi.baidu.com/gettts?lan=${lan}&text=${encodeURIComponent(s)}&spd=3&source=web`
}
let r = []
let arr = sliceStr(q, 128)
arr.forEach(text => {
r.push(getUrl(text))
})
resolve(r)
})
},
link(q, srcLan, tarLan) {
return `https://fanyi.baidu.com/#${srcLan}/${tarLan}/${encodeURIComponent(q)}`
},
}
}
================================================
FILE: src/js/translate/bing.js
================================================
'use strict'
/**
* Dream Translate
* https://github.com/ryanker/dream_translate
* @Author Ryan
* @license MIT License
*/
function bingTranslate() {
return {
token: {
ig: '',
iid: '',
num: 0,
date: 0,
paramsToken: '',
paramsKey: 0,
ttsToken: '',
ttsRegion: '',
ttsExpiry: 0,
},
langCheck: '',
langMap: {
"auto": "auto-detect",
"yue": "yue",
"cs": "cs",
"nl": "nl",
"en": "en",
"fil": "fil",
"de": "de",
"el": "el",
"ht": "ht",
"hi": "hi",
"hu": "hu",
"id": "id",
"it": "it",
"mg": "mg",
"pl": "pl",
"ro": "ro",
"ru": "ru",
"sm": "sm",
"sk": "sk",
"th": "th",
"tr": "tr",
"afr": "af",
"ara": "ar",
"asm": "as",
"bos": "bs",
"bul": "bg",
"cat": "ca",
"hrv": "hr",
"dan": "da",
"est": "et",
"fin": "fi",
"fra": "fr",
"guj": "gu",
"heb": "he",
"ice": "is",
"gle": "ga",
"jp": "ja",
"kan": "kn",
"kaz": "kk",
"kor": "ko",
"lav": "lv",
"lit": "lt",
"may": "ms",
"mal": "ml",
"mlt": "mt",
"mao": "mi",
"mar": "mr",
"nor": "nb",
"pus": "ps",
"per": "fa",
"pan": "pa",
"slo": "sl",
"spa": "es",
"swa": "sw",
"swe": "sv",
"tam": "ta",
"tel": "te",
"ukr": "uk",
"urd": "ur",
"vie": "vi",
"wel": "cy",
"zh": "zh-Hans",
"cht": "zh-Hant",
"frn": "fr-ca",
"hmn": "mww",
"pot": "pt",
"pt": "pt-pt",
"srp": "sr-Latn"
},
langMapInvert: {},
lanTTS: ["zh", "en", "jp", "th", "spa", "ara", "fra", "kor", "ru", "de", "pt", "it", "el", "nl", "pl", "fin", "cs", "bul"],
init() {
this.langMapInvert = invertObject(this.langMap)
let str = localStorage.getItem('bingToken')
if (str) this.token = JSON.parse(str)
return this
},
setToken(options) {
this.token = Object.assign(this.token, options)
localStorage.setItem('bingToken', JSON.stringify(this.token))
},
getToken() {
return new Promise((resolve, reject) => {
httpGet('https://cn.bing.com/translator').then(r => {
let arr = r.match(/,IG:"([^"]+)",/)
let tArr = r.match(/_iid="([^"]+)"/)
let paramsArr = r.match(/var params_RichTranslateHelper = \[(\d+),"([^"]+)",/)
if (!arr) return reject('bing IG empty!')
if (!tArr) return reject('bing IID empty!')
if (!paramsArr) return reject('bing paramsArr empty!')
let token = {
ig: arr[1],
iid: tArr[1],
num: 0,
paramsToken: paramsArr[2],
paramsKey: paramsArr[1],
date: Math.floor(Date.now() / 36e5)
}
this.setToken(token)
resolve(token)
}).catch(e => {
reject(e)
})
})
},
trans(q, srcLan, tarLan) {
srcLan = this.langMap[srcLan] || 'auto-detect'
tarLan = this.langMap[tarLan] || 'zh-Hans'
return new Promise((resolve, reject) => {
if (q.length > 5000) return reject('The text is too large!')
if (!this.token.ig) return reject('bing ig empty!')
if (!this.token.iid) return reject('bing iid empty!')
let ig = this.token.ig
let iid = this.token.iid
let num = ++this.token.num
let paramsToken = this.token.paramsToken
let paramsKey = this.token.paramsKey
let url = `https://cn.bing.com/ttranslatev3?isVertical=1&&IG=${ig}&IID=${iid}.${num}`
let p = new URLSearchParams(`&fromLang=${srcLan}&text=${q}&to=${tarLan}&token=${paramsToken}&key=${paramsKey}`)
httpPost({url: url, body: p.toString()}).then(r => {
if (r) {
resolve(this.unify(r, q, srcLan, tarLan))
} else {
reject('bing trans error!')
}
}).catch(e => {
reject(e)
})
})
},
unify(r, q, srcLan, tarLan) {
// console.log('bing:', r, q, srcLan, tarLan)
if (srcLan === 'auto-detect' && r[0].detectedLanguage) srcLan = r[0].detectedLanguage.language
let map = this.langMapInvert
srcLan = map[srcLan] || 'auto'
tarLan = map[tarLan] || ''
let ret = {text: q, srcLan: srcLan, tarLan: tarLan, lanTTS: this.lanTTS, data: []}
let srcArr = q.split('\n')
let tarArr = []
let arr = r && r[0] && r[0].translations
arr && arr.forEach(v => {
if (v.text) tarArr = Object.assign(tarArr, v.text.split('\n'))
})
tarArr.forEach((v, k) => {
ret.data.push({srcText: srcArr[k] || '', tarText: v})
})
return ret
},
async query(q, srcLan, tarLan, noCache) {
return checkRetry(async (i) => {
let t = Math.floor(Date.now() / 36e5)
let d = this.token.date
if (i > 0) noCache = true
if (noCache || !d || Number(d) !== t) {
await this.getToken().catch(err => console.warn(err))
}
return this.trans(q, srcLan, tarLan)
})
},
tts(q, lan) {
// see: https://docs.microsoft.com/zh-cn/azure/cognitive-services/speech-service/language-support
// 英语(美国) en-US Female en-US-JessaRUS
// 普通话(简体中文,中国) zh-CN Female zh-CN-HuihuiRUS
let arr = {
zh: {lang: 'zh-CN', gender: 'Female', name: 'zh-CN-HuihuiRUS'},
en: {lang: 'en-US', gender: 'Female', name: 'en-US-JessaRUS'},
jp: {lang: 'ja-JP', gender: 'Female', name: 'ja-JP-Ayumi'},
th: {lang: 'th-TH', gender: 'Male', name: 'th-TH-Pattara'},
spa: {lang: 'es-ES', gender: 'Female', name: 'es-ES-Laura'},
ara: {lang: 'ar-SA', gender: 'Male', name: 'ar-SA-Naayf'},
fra: {lang: 'fr-FR', gender: 'Female', name: 'fr-FR-Julie-Apollo'},
kor: {lang: 'ko-KR', gender: 'Female', name: 'ko-KR-HeamiRUS'},
ru: {lang: 'ru-RU', gender: 'Female', name: 'ru-RU-Irina-Apollo'},
de: {lang: 'de-DE', gender: 'Female', name: 'de-DE-Hedda'},
pt: {lang: 'pt-PT', gender: 'Female', name: 'pt-PT-HeliaRUS'},
it: {lang: 'it-IT', gender: 'Female', name: 'it-IT-Cosimo-Apollo'},
el: {lang: 'el-GR', gender: 'Male', name: 'el-GR-Stefanos'},
nl: {lang: 'nl-NL', gender: 'Female', name: 'nl-NL-HannaRUS'},
pl: {lang: 'pl-PL', gender: 'Female', name: 'pl-PL-PaulinaRUS'},
fin: {lang: 'fi-FI', gender: 'Female', name: 'fi-FI-HeidiRUS'},
cs: {lang: 'cs-CZ', gender: 'Male', name: 'cs-CZ-Jakub'},
bul: {lang: 'bg-BG', gender: 'Male', name: 'bg-BG-Ivan'},
}
return new Promise((resolve, reject) => {
if (!inArray(lan, this.lanTTS)) return reject('This language is not supported!')
let l = arr[lan] || arr.en
if (!this.token.ig) return reject('bing ig empty!')
if (!this.token.iid) return reject('bing iid empty!')
let ig = this.token.ig
let iid = this.token.iid
let num = this.token.num
let paramsToken = this.token.paramsToken
let paramsKey = this.token.paramsKey
let ttsToken = this.token.ttsToken
let ttsRegion = this.token.ttsRegion
let expiry = this.token.ttsExpiry
let ttsBlob = (q, ttsToken, ttsRegion) => {
httpPost({
url: `https://${ttsRegion}.tts.speech.microsoft.com/cognitiveservices/v1`,
type: 'xml',
responseType: 'blob',
headers: [
{name: 'X-MICROSOFT-OutputFormat', value: 'audio-16khz-32kbitrate-mono-mp3'},
{name: 'Authorization', value: `Bearer ${ttsToken}`},
],
body: `${q}`,
}).then(r => {
if (r) {
resolve(r)
} else {
reject('bing tts api error!')
}
}).catch(e => {
reject(e)
})
}
let t = Math.floor(Date.now() / 1000)
if (expiry - 60 > t) {
ttsBlob(q, ttsToken, ttsRegion)
} else {
let p = new URLSearchParams(`token=${paramsToken}&key=${paramsKey}`)
httpPost({
url: `https://cn.bing.com/tfetspktok?isVertical=1&=&IG=${ig}&IID=${iid}.${num}`,
body: p.toString()
}).then(r => {
if (r && r.token && r.region && r.expiry && r.statusCode === 200) {
this.setToken({ttsToken: r.token, ttsRegion: r.region, ttsExpiry: r.expiry * 1})
ttsBlob(q, r.token, r.region)
} else {
reject('bing tts token api error!')
}
}).catch(e => {
reject(e)
})
}
})
},
link(q, srcLan, tarLan) {
return `https://cn.bing.com/translator?d_sl=${srcLan}&d_tl=${tarLan}&d_text=${encodeURI(q)}`
},
}
}
================================================
FILE: src/js/translate/deepl.js
================================================
'use strict'
/**
* Dream Translate
* https://github.com/ryanker/dream_translate
* @Author Ryan
* @license MIT License
*/
function deeplTranslate() {
return {
langMap: {
"auto": "auto",
"zh": "zh",
"en": "en",
"de": "de",
"fra": "fr",
"spa": "es",
"pt": "pt",
"it": "it",
"nl": "nl",
"pl": "pl",
"ru": "ru",
"jp": "ja",
},
langMapInvert: {},
isData: false,
init() {
this.langMapInvert = invertObject(this.langMap)
return this
},
trans(q, srcLan, tarLan) {
srcLan = this.langMap[srcLan] || 'auto'
tarLan = this.langMap[tarLan] || 'zh'
return new Promise((resolve, reject) => {
if (q.length > 5000) return reject('The text is too large!')
// 取消 Frame 嵌入限制
onHeadersReceivedAddListener(onRemoveFrame, {urls: ["*://*.deepl.com/*"]})
// popup 框
let pageId = 'fy_DeepL'
let url = `https://www.deepl.com/translator#${srcLan}/${tarLan}/${encodeURI(q)}`
// console.log('url:', url)
openIframe(pageId, url, 60 * 1000)
// 获取请求参数
let filter = {urls: ['*://*.deepl.com/jsonrpc*'], types: ['xmlhttprequest']} // 请求参数
this.isData = false
let onBeforeRequest = (details) => {
if (this.isData) return
let {requestBody, url} = details
let bytes = getJSONValue(requestBody, 'raw.0.bytes')
if (!bytes) return
let body = new TextDecoder().decode(bytes)
// 获取数据
_setTimeout('trans_DeepL', () => {
onBeforeSendHeadersAddListener(onBeforeSendHeaders, filter)
let options = {url, body, type: 'json'}
httpPost(options).then(r => {
removeListener()
if (r) {
// 超时报错
let outId = _setTimeout('trans_DeepL_reject', () => {
reject('DeepL result error!')
}, 20 * 1000)
let res = this.unify(r, q, srcLan, tarLan)
if (res.data && res.data.length > 0) {
if (this.isData) return
this.isData = true // 表示有数据了
resolve(res)
_clearTimeout(outId)
}
} else {
reject('DeepL error!')
}
}).catch(e => {
removeListener()
reject(e)
})
}, 200)
// return {cancel: true}
}
onBeforeRequestAddListener(onBeforeRequest, filter)
// 请求接口数据修改
function onBeforeSendHeaders(details) {
let h = details.requestHeaders
h.push({name: 'Host', value: 'www.deepl.com'})
h.push({name: 'Origin', value: 'https://www.deepl.com'})
h.push({name: 'Referer', value: 'https://www.deepl.com/'})
h.push({name: 'sec-fetch-dest', value: 'document'})
h.push({name: 'sec-fetch-mode', value: 'navigate'})
h.push({name: 'sec-fetch-site', value: 'same-origin'})
return {requestHeaders: h}
}
// 销毁
function removeListener() {
onHeadersReceivedRemoveListener(onRemoveFrame)
onBeforeSendHeadersRemoveListener(onBeforeSendHeaders)
onBeforeRequestRemoveListener(onBeforeRequest)
}
})
},
unify(r, text, srcLan, tarLan) {
// console.log('DeepL:', r, text, srcLan, tarLan)
// console.log(JSON.stringify(r))
// v1.0 2021.1.10
if (srcLan === 'auto' && r.source_lang) srcLan = r.source_lang.toLowerCase()
let map = this.langMapInvert
srcLan = map[srcLan] || 'auto'
tarLan = map[tarLan] || ''
let data = []
let extra = ''
let trans = getJSONValue(r, 'result.translations')
if (trans && trans.length > 0) {
let srcArr = text.split('\n')
trans.forEach(tv => {
if (!tv.beams) return
tv.beams.forEach((v, k) => {
let tarText = v.postprocessed_sentence
if (tarText) {
if (k === 0) {
data.push({srcText: srcArr[k] || '', tarText})
} else {
extra += `
${tarText}
`
}
}
})
})
}
if (extra) extra = `
${extra}
`
return {text, srcLan, tarLan, lanTTS: this.lanTTS, data, extra}
},
async query(q, srcLan, tarLan) {
return checkRetry(() => this.trans(q, srcLan, tarLan), 1)
},
tts(q, lan) {
lan = this.langMap[lan] || 'en'
return new Promise((resolve) => {
let getUrl = (s) => {
return `https://fanyi.sogou.com/reventondc/synthesis?text=${encodeURI(s)}&speed=1&lang=${lan}&from=translateweb&speaker=4`
}
let r = []
let arr = sliceStr(q, 128)
arr.forEach(text => {
r.push(getUrl(text))
})
resolve(r)
})
},
link(q, srcLan, tarLan) {
srcLan = this.langMap[srcLan] || 'auto'
tarLan = this.langMap[tarLan] || 'zh'
return `https://www.deepl.com/translator#${srcLan}/${tarLan}/${encodeURI(q)}`
},
}
}
================================================
FILE: src/js/translate/frdic.js
================================================
'use strict'
/**
* Dream Translate
* https://github.com/ryanker/dream_translate
* @Author Ryan
* @license MIT License
*/
function frdicTranslate() {
return {
init() {
return this
},
addListenerRequest() {
onBeforeSendHeadersAddListener(this.onChangeHeaders, {urls: ['*://api.frdic.com/api/*']})
},
removeListenerRequest() {
onBeforeSendHeadersRemoveListener(this.onChangeHeaders)
},
onChangeHeaders(details) {
let h = details.requestHeaders
h.push({name: 'Origin', value: 'https://dict.eudic.net/'})
h.push({name: 'Referer', value: 'https://dict.eudic.net/'})
return {requestHeaders: h}
},
onRequest() {
this.addListenerRequest()
if (this.timeoutId) {
clearTimeout(this.timeoutId)
this.timeoutId = null
}
this.timeoutId = setTimeout(this.removeListenerRequest, 30000)
},
encode(s) {
let Base64 = {
_keyStr: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
encode: function (n) {
let f = '', e, t, i, s, h, o, r, u = 0
for (n = Base64._utf8_encode(n); u < n.length;)
e = n.charCodeAt(u++), t = n.charCodeAt(u++), i = n.charCodeAt(u++), s = e >> 2,
h = (e & 3) << 4 | t >> 4, o = (t & 15) << 2 | i >> 6, r = i & 63, isNaN(t) ? o = r = 64 : isNaN(i) && (r = 64),
f = f + Base64._keyStr.charAt(s) + Base64._keyStr.charAt(h) + Base64._keyStr.charAt(o) + Base64._keyStr.charAt(r)
return f
},
decode: function (n) {
let t = '', e, o, s, h, u, r, f, i = 0
for (n = n.replace(/[^A-Za-z0-9\+\/\=]/g, ""); i < n.length;)
h = Base64._keyStr.indexOf(n.charAt(i++)),
u = Base64._keyStr.indexOf(n.charAt(i++)),
r = Base64._keyStr.indexOf(n.charAt(i++)),
f = Base64._keyStr.indexOf(n.charAt(i++)),
e = h << 2 | u >> 4,
o = (u & 15) << 4 | r >> 2,
s = (r & 3) << 6 | f,
t = t + String.fromCharCode(e),
r !== 64 && (t = t + String.fromCharCode(o)),
f !== 64 && (t = t + String.fromCharCode(s))
return Base64._utf8_decode(t)
},
_utf8_encode: function (n) {
let i, r, t
for (n = n.replace(/\r\n/g, "\n"), i = "", r = 0; r < n.length; r++)
t = n.charCodeAt(r), t < 128 ? i += String.fromCharCode(t) : t > 127 && t < 2048 ?
(i += String.fromCharCode(t >> 6 | 192), i += String.fromCharCode(t & 63 | 128)) :
(i += String.fromCharCode(t >> 12 | 224), i += String.fromCharCode(t >> 6 & 63 | 128), i += String.fromCharCode(t & 63 | 128))
return i
},
_utf8_decode: function (n) {
let r = '', t = 0, i = 0, c2 = 0, c3 = 0
for (; t < n.length;)
i = n.charCodeAt(t), i < 128 ? (r += String.fromCharCode(i), t++) : i > 191 && i < 224 ?
(c2 = n.charCodeAt(t + 1), r += String.fromCharCode((i & 31) << 6 | c2 & 63), t += 2) :
(c2 = n.charCodeAt(t + 1), c3 = n.charCodeAt(t + 2), r += String.fromCharCode((i & 15) << 12 | (c2 & 63) << 6 | c3 & 63), t += 3)
return r
}
}
let fix = function (s) {
return encodeURI(s).replace(/[!'()]/g, escape).replace(/\*/g, "%2A")
}
return fix(Base64.encode(s))
},
tts(q, lan) {
return new Promise((resolve, reject) => {
if (lan === 'auto') lan = 'en'
let lanArr = {'en': 'en', 'zh': 'zh', 'fra': 'fr', 'de': 'de', 'spa': 'es', 'jp': 'jp'}
if (!lanArr[lan]) return reject('This language is not supported!')
lan = lanArr[lan]
let getUrl = (s) => {
return `https://api.frdic.com/api/v2/speech/speakweb?langid=${lan}&txt=QYN${this.encode(s)}`
}
let r = []
let arr = sliceStr(q, 128)
arr.forEach(text => {
r.push(getUrl(text))
})
this.onRequest()
resolve(r)
})
},
}
}
================================================
FILE: src/js/translate/google.js
================================================
'use strict'
/**
* Dream Translate
* https://github.com/ryanker/dream_translate
* @Author Ryan
* @license MIT License
*/
function googleTranslate() {
return {
langMap: {
"auto": "auto",
"pl": "pl",
"de": "de",
"ru": "ru",
"ht": "ht",
"nl": "nl",
"cs": "cs",
"ro": "ro",
"mg": "mg",
"hmn": "hmn",
"pt": "pt",
"sm": "sm",
"sk": "sk",
"ceb": "ceb",
"th": "th",
"tr": "tr",
"el": "el",
"haw": "haw",
"hu": "hu",
"it": "it",
"hi": "hi",
"id": "id",
"en": "en",
"alb": "sq",
"ara": "ar",
"amh": "am",
"aze": "az",
"gle": "ga",
"est": "et",
"baq": "eu",
"bel": "be",
"bul": "bg",
"ice": "is",
"bos": "bs",
"per": "fa",
"tat": "tt",
"dan": "da",
"fra": "fr",
"fil": "tl",
"fin": "fi",
"hkm": "km",
"geo": "ka",
"guj": "gu",
"kaz": "kk",
"kor": "ko",
"hau": "ha",
"kir": "ky",
"glg": "gl",
"cat": "ca",
"kan": "kn",
"cos": "co",
"hrv": "hr",
"kur": "ku",
"lat": "la",
"lav": "lv",
"lao": "lo",
"lit": "lt",
"ltz": "lb",
"kin": "rw",
"mlt": "mt",
"mar": "mr",
"mal": "ml",
"may": "ms",
"mac": "mk",
"mao": "mi",
"ben": "bn",
"bur": "my",
"nep": "ne",
"nor": "no",
"pan": "pa",
"pus": "ps",
"nya": "ny",
"jp": "ja",
"swe": "sv",
"sin": "si",
"epo": "eo",
"slo": "sl",
"swa": "sw",
"som": "so",
"tgk": "tg",
"tel": "te",
"tam": "ta",
"tuk": "tk",
"wel": "cy",
"urd": "ur",
"ukr": "uk",
"uzb": "uz",
"spa": "es",
"heb": "iw",
"snd": "sd",
"sna": "sn",
"arm": "hy",
"ibo": "ig",
"yid": "yi",
"yor": "yo",
"vie": "vi",
"afr": "af",
"xho": "xh",
"zul": "zu",
"srp": "sr",
"jav": "jw",
"zh": "zh-CN",
"fry": "fy",
"sco": "gd",
"sun": "su",
"or": "or",
"mn": "mn",
"st": "st",
"ug": "ug"
},
langMapInvert: {},
init() {
this.langMapInvert = invertObject(this.langMap)
return this
},
unify(r, q, srcLan, tarLan) {
// console.log('google:', r, q, srcLan, tarLan)
// 翻译的语言参数
if (srcLan === 'auto' && r.sourceLanguage) srcLan = r.sourceLanguage; // 源语言
let map = this.langMapInvert
srcLan = map[srcLan] || 'auto'
tarLan = map[tarLan] || ''
// 翻译结果
let data = [];
r.sentences && r.sentences.forEach(v => {
if (v.trans && v.orig) data.push({srcText: v.orig, tarText: v.trans})
})
// 额外信息,如单词释义等
let extra = '';
if (!setting.translateThin && r.bilingualDictionary && isArray(r.bilingualDictionary)) {
r.bilingualDictionary.forEach(v => {
if (v.pos && v.entry) {
let entryArr = [];
if (isArray(v.entry) && v.entry.length > 0) {
v.entry.map(v => {
entryArr.push(v.word);
});
}
if (entryArr.length > 0) {
extra += `
${v.pos}${entryArr.join(';')}
`
}
}
})
if (extra) extra = `
${extra}
`
}
return {
text: q, // 需要翻译的原始文本
srcLan: srcLan, // 源语言代码,如 en, zh-CN 等
tarLan: tarLan, // 目标语言代码,如 en, zh-CN 等
lanTTS: null,
data: data, // 翻译结果,如 [{srcText: 'hello', tarText: '你好'}]
extra: extra, // 额外信息,如单词释义等
}
},
trans(q, srcLan, tarLan) {
srcLan = this.langMap[srcLan] || 'auto'
tarLan = this.langMap[tarLan] || 'zh-CN'
return new Promise(async (resolve, reject) => {
if (q.length > 1000) return reject('The text is too large!')
// 翻译接口来源于,官方的 Google 翻译插件
const url = `https://translate-pa.googleapis.com/v1/translate?params.client=gtx` +
`&query.source_language=${srcLan}` +
`&query.target_language=${tarLan}` +
`&query.display_language=${tarLan}` +
`&query.text=${encodeURIComponent(q)}` +
'&key=AIzaSyDLEeFI5OtFBwYBIoK_jj5m32rZK5CkCXA' +
'&data_types=TRANSLATION' +
'&data_types=SENTENCE_SPLITS' +
'&data_types=BILINGUAL_DICTIONARY_FULL';
await httpGet(url, 'json').then(r => {
if (r) {
resolve(this.unify(r, q, srcLan, tarLan))
} else {
reject('google translate error!')
}
}).catch(function (e) {
reject(e)
})
})
},
async query(q, srcLan, tarLan) {
return checkRetry(() => this.trans(q, srcLan, tarLan), 2)
},
tts(q, lan) {
lan = this.langMap[lan] || 'en'
return new Promise(async (resolve, reject) => {
try {
const url = 'https://translate-pa.googleapis.com/v1/textToSpeech?client=gtx' +
'&language=' + lan +
'&text=' + encodeURIComponent(q) +
'&voice_speed=1' +
'&key=AIzaSyDLEeFI5OtFBwYBIoK_jj5m32rZK5CkCXA';
let data = await httpGet(url, 'json');
let blobUrl = this.base64ToBlobUrl(data.audioContent); // 将 Base64 编码的数据转换为 Blob 对象并创建一个指向该 Blob 的 URL
resolve([blobUrl])
} catch (e) {
reject(e)
}
})
},
// 将 Base64 编码的数据转换为 Blob 对象并创建一个指向该 Blob 的 URL
base64ToBlobUrl(base64Data) {
const base64WithoutPrefix = base64Data.replace(/^data:.+;base64,/, '');
const binaryData = atob(base64WithoutPrefix);
const arrayBuffer = new ArrayBuffer(binaryData.length);
const uint8Array = new Uint8Array(arrayBuffer);
for (let i = 0; i < binaryData.length; i++) {
uint8Array[i] = binaryData.charCodeAt(i);
}
const blob = new Blob([uint8Array], {type: 'audio/mp3'});
return URL.createObjectURL(blob);
},
link(q, srcLan, tarLan) {
srcLan = this.langMap[srcLan] || 'auto'
tarLan = this.langMap[tarLan] || 'zh-CN'
return `https://translate.google.com/?sl=${srcLan}&tl=${tarLan}&text=${encodeURIComponent(q)}&op=translate`
},
}
}
================================================
FILE: src/js/translate/local.js
================================================
'use strict'
/**
* Dream Translate
* https://github.com/ryanker/dream_translate
* @Author Ryan
* @license MIT License
*/
function localTranslate() {
return {
voiceList: null,
init() {
return this
},
tts(q, lan) {
return new Promise(async (resolve, reject) => {
if (!this.voiceList) await getVoices().then(r => this.voiceList = r)
let ttsConf = setting.ttsConf || {}
let lang = getJSONValue(conf, `ttsList.${lan}`)
if (!lang || !this.voiceList || !this.voiceList[lang]) return reject('This language is not supported!')
let options = {}
if (ttsConf['speak_rate']) options.rate = Number(ttsConf['speak_rate'])
if (ttsConf['speak_pitch']) options.pitch = Number(ttsConf['speak_pitch'])
if (ttsConf[lang]) {
options.voiceName = ttsConf[lang]
} else if (['en-US', 'es-ES', 'nl-NL'].includes(lang)) {
let a = {'en-US': 'en', 'es-ES': 'es', 'nl-NL': 'nl'}
lang = a[lang]
if (ttsConf[lang]) {
options.voiceName = ttsConf[lang]
} else {
options.lang = lang
}
} else {
options.lang = lang
}
let arr = sliceStr(q, 128)
let lastKey = arr.length - 1
arr.forEach((v, k) => {
options.onEvent = function (e) {
// console.log('onEvent:', lastKey, k, v, e.type, options)
if (e.type === 'end') {
if (k === lastKey) resolve()
} else if (e.type === 'error') {
debug('tts.speak error:', e.errorMessage)
reject(e.errorMessage)
}
}
if (k === 0) {
B.tts.speak(v, options)
} else {
B.tts.speak(v, Object.assign({enqueue: true}, options))
}
})
})
},
}
}
================================================
FILE: src/js/translate/qq.js
================================================
'use strict'
/**
* Dream Translate
* https://github.com/ryanker/dream_translate
* @Author Ryan
* @license MIT License
*/
function qqTranslate() {
return {
token: {
qtv: '',
qtk: '',
},
cookie: {},
langMap: {
"auto": "auto",
"zh": "zh",
"en": "en",
"jp": "jp",
"it": "it",
"de": "de",
"tr": "tr",
"ru": "ru",
"pt": "pt",
"id": "id",
"th": "th",
"hi": "hi",
"kor": "kr",
"fra": "fr",
"spa": "es",
"vie": "vi",
"ara": "ar",
"may": "ms"
},
langMapInvert: {},
pairMap: {
auto: ["zh", "en", "jp", "kr", "fr", "es", "it", "de", "tr", "ru", "pt", "vi", "id", "th", "ms"],
en: ["zh", "fr", "es", "it", "de", "tr", "ru", "pt", "vi", "id", "th", "ms", "ar", "hi"],
zh: ["en", "jp", "kr", "fr", "es", "it", "de", "tr", "ru", "pt", "vi", "id", "th", "ms"],
fr: ["zh", "en", "es", "it", "de", "tr", "ru", "pt"],
es: ["zh", "en", "fr", "it", "de", "tr", "ru", "pt"],
it: ["zh", "en", "fr", "es", "de", "tr", "ru", "pt"],
de: ["zh", "en", "fr", "es", "it", "tr", "ru", "pt"],
tr: ["zh", "en", "fr", "es", "it", "de", "ru", "pt"],
ru: ["zh", "en", "fr", "es", "it", "de", "tr", "pt"],
pt: ["zh", "en", "fr", "es", "it", "de", "tr", "ru"],
vi: ["zh", "en"],
id: ["zh", "en"],
ms: ["zh", "en"],
th: ["zh", "en"],
jp: ["zh"],
kr: ["zh"],
ar: ["en"],
hi: ["en"]
},
init() {
this.langMapInvert = invertObject(this.langMap)
let str = localStorage.getItem('qqToken')
if (str) this.token = JSON.parse(str)
this.getCookieAll()
// 30s刷新页面
setInterval(() => {
this.getToken().catch(err => debug('qq getToken error:', err))
}, 30 * 1000)
return this
},
setToken(options) {
this.token = Object.assign(this.token, options)
localStorage.setItem('qqToken', JSON.stringify(this.token))
},
getToken() {
return new Promise((resolve, reject) => {
httpGet('https://fanyi.qq.com/').then(r => {
let arr = r.match(/var reauthuri = "(.+)";/)
if (!arr) return reject('qq reauthuri empty!')
let reauthuri = arr[1]
let qtv = this.token.qtv
let qtk = this.token.qtk
let body = ''
if (qtv && qtk) body = `qtv=${this.rep(qtv)}&qtk=${this.rep(qtk)}`
httpPost({url: 'https://fanyi.qq.com/api/' + reauthuri, body: body}).then(r => {
if (r) {
let token = {qtv: r.qtv, qtk: r.qtk}
this.setToken(token)
this.setCookie('qtk', r.qtk)
this.setCookie('qtv', r.qtv)
resolve(token)
} else {
reject('qq reaauth error!')
}
}).catch(e => {
reject(e)
})
}).catch(e => {
reject(e)
})
})
},
rep(s) {
return s.replace(/\+/g, '%2B')
},
// addListenerRequest() {
// onBeforeSendHeadersAddListener(this.onChangeHeaders, {urls: ['*://fanyi.qq.com/api/*']})
// },
/*onChangeHeaders(details) {
// 获取最新 auth 链接
if (details.url && details.url.includes('auth')) {
localStorage['qqAuthUrl'] = details.url
}
let s = `Host: fanyi.qq.com
Origin: https://fanyi.qq.com
Referer: https://fanyi.qq.com
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin`
return {requestHeaders: details.requestHeaders.concat(requestHeadersFormat(s))}
},*/
trans(q, srcLan, tarLan) {
srcLan = this.langMap[srcLan] || 'auto'
tarLan = this.langMap[tarLan] || 'zh'
if (!inArray(tarLan, this.pairMap[srcLan])) tarLan = this.pairMap[srcLan][0]
return new Promise((resolve, reject) => {
if (q.length > 5000) return reject('The text is too large!')
let qtv = this.token.qtv
let qtk = this.token.qtk
let uuid = 'translate_uuid' + (new Date).getTime()
let p = `source=${srcLan}&target=${tarLan}&sourceText=${encodeURI(q)}&qtv=${this.rep(qtv)}&qtk=${this.rep(qtk)}&ticket=&randstr=&sessionUuid=${uuid}`
httpPost({url: 'https://fanyi.qq.com/api/translate', body: p}).then(r => {
if (r) {
resolve(this.unify(r, q, srcLan, tarLan))
} else {
reject('qq translate error!')
}
}).catch(e => {
reject(e)
})
})
},
unify(r, q, srcLan, tarLan) {
// console.log('qq:', r, q, srcLan, tarLan)
if (srcLan === 'auto' && r.translate && r.translate.source) srcLan = r.translate.source
let map = this.langMapInvert
srcLan = map[srcLan] || 'auto'
tarLan = map[tarLan] || ''
let ret = {text: q, srcLan: srcLan, tarLan: tarLan, lanTTS: null, data: []}
let arr = r && r.translate && r.translate.records
arr && arr.forEach(v => {
let srcText = v.sourceText ? v.sourceText.trim() : ''
let tarText = v.targetText ? v.targetText.trim() : ''
if (srcText && tarText) ret.data.push({srcText: srcText, tarText: tarText})
})
return ret
},
async query(q, srcLan, tarLan) {
return checkRetry(async () => {
return this.trans(q, srcLan, tarLan)
}, 2)
},
setCookie(name, value) {
let domain = 'fanyi.qq.com'
cookies('set', {url: `https://${domain}`, name: name, value: value, domain: domain, path: '/'}).then(v => {
this.cookie[v.name] = v.value
})
},
getCookieAll(callback) {
cookies('getAll', {domain: 'fanyi.qq.com'}).then(arr => {
arr.forEach(v => {
this.cookie[v.name] = v.value
})
typeof callback === 'function' && callback()
})
},
getCookie(name) {
return this.cookie[name] || ''
},
tts(q, lan) {
lan = this.langMap[lan] || 'en'
return new Promise((resolve) => {
let guid = this.getCookie('fy_guid')
// todo: 腾讯 TTS 服务很不稳定
resolve(`https://fanyi.qq.com/api/tts?platform=PC_Website&lang=${lan}&text=${encodeURI(q)}&guid=${guid}`)
})
},
link(q, srcLan, tarLan) {
return `https://fanyi.qq.com/?d_sl=${srcLan}&d_tl=${tarLan}&d_text=${encodeURI(q)}`
},
}
}
================================================
FILE: src/js/translate/so.js
================================================
'use strict'
/**
* Dream Translate
* https://github.com/ryanker/dream_translate
* @Author Ryan
* @license MIT License
*/
function soTranslate() {
return {
data: {},
init() {
return this
},
addListenerRequest() {
onBeforeSendHeadersAddListener(this.onChangeHeaders, {urls: ['*://fanyi.so.com/*']})
},
removeListenerRequest() {
onBeforeSendHeadersRemoveListener(this.onChangeHeaders)
},
onChangeHeaders(details) {
let s = `Host: fanyi.so.com
Origin: https://fanyi.so.com
pro: fanyi
Referer: https://fanyi.so.com/
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin`
return {requestHeaders: details.requestHeaders.concat(requestHeadersFormat(s))}
},
trans(q, srcLan, tarLan) {
if (/\p{Script=Han}/u.test(q)) srcLan = 'zh'
tarLan = srcLan === 'zh' ? 'en' : 'zh'
let eng = srcLan === 'en' ? 1 : 0
return new Promise((resolve, reject) => {
if (q.length > 5000) return reject('The text is too large!')
this.addListenerRequest()
let url = `https://fanyi.so.com/index/search?eng=${eng}&validate=&ignore_trans=0&query=${encodeURI(q)}`
let p = new URLSearchParams(`eng=${eng}&validate=&ignore_trans=0&query=${encodeURI(q)}`)
httpPost({url: url, body: p.toString()}).then(r => {
this.removeListenerRequest()
if (r) {
resolve(this.unify(r, q, srcLan, tarLan))
} else {
reject('so trans error!')
}
}).catch(e => {
this.removeListenerRequest()
reject(e)
})
})
},
unify(r, text, srcLan, tarLan) {
// console.log('so:', r, q, srcLan, tarLan)
let ret = {text, srcLan, tarLan, lanTTS: null, data: []}
let data = r && r.data
if (data) {
this.data = data
if (data.fanyi) ret.data.push({srcText: text, tarText: data.fanyi})
}
return ret
},
async query(q, srcLan, tarLan) {
return checkRetry(() => this.trans(q, srcLan, tarLan))
},
tts(q, lan) {
return new Promise((resolve, reject) => {
let isEn = lan === 'en'
let r = this.data && this.data.speak_url
if (r) {
let arr = {}
if (r.word_type === 'en2zh') {
arr['en'] = r.speak_url
arr['zh'] = r.tSpeak_url
} else {
arr['zh'] = r.speak_url
arr['en'] = r.tSpeak_url
}
resolve(`https://fanyi.so.com` + (isEn ? arr['en'] : arr['zh']))
} else {
reject('speak url empty')
}
})
},
link(q, srcLan, tarLan) {
return `https://fanyi.so.com/?src=dream_translate#${q}`
},
}
}
================================================
FILE: src/js/translate/sogou.js
================================================
'use strict'
/**
* Dream Translate
* https://github.com/ryanker/dream_translate
* @Author Ryan
* @license MIT License
*/
function sogouTranslate() {
return {
langMap: {
"auto": "auto",
"pl": "pl",
"de": "de",
"ru": "ru",
"fil": "fil",
"ht": "ht",
"nl": "nl",
"cs": "cs",
"ro": "ro",
"mg": "mg",
"pt": "pt",
"sk": "sk",
"sm": "sm",
"th": "th",
"tr": "tr",
"el": "el",
"hu": "hu",
"en": "en",
"it": "it",
"hi": "hi",
"id": "id",
"yue": "yue",
"ara": "ar",
"est": "et",
"bul": "bg",
"bos": "bs-Latn",
"per": "fa",
"dan": "da",
"fra": "fr",
"fin": "fi",
"kor": "ko",
"kli": "tlh",
"hrv": "hr",
"lav": "lv",
"lit": "lt",
"may": "ms",
"mlt": "mt",
"ben": "bn",
"afr": "af",
"nor": "no",
"jp": "ja",
"swe": "sv",
"slo": "sl",
"srp": "sr-Latn",
"src": "sr-Cyrl",
"swa": "sw",
"wel": "cy",
"ukr": "uk",
"urd": "ur",
"spa": "es",
"heb": "he",
"vie": "vi",
"cat": "ca",
"zh": "zh-CHS",
"cht": "zh-CHT"
},
langMapInvert: {},
init() {
this.langMapInvert = invertObject(this.langMap)
return this
},
// 2020.12.03 刚写完就改版,白破解了!要吐了。。。
/*transOld(q, srcLan, tarLan) {
srcLan = this.langMap[srcLan] || 'auto'
tarLan = this.langMap[tarLan] || 'zh-CHS'
return new Promise((resolve, reject) => {
if (q.length > 5000) return reject('The text is too large!')
// 取消 Frame 嵌入限制
onHeadersReceivedAddListener(onRemoveFrame, {urls: ["*://fanyi.sogou.com/*"]})
// Frame 请求
let url = `https://fanyi.sogou.com/?keyword=${encodeURI(q)}&transfrom=${srcLan}&transto=${tarLan}&model=general`
openIframe('iframe_soGou', url)
// 获取请求参数
let urls = ['*://fanyi.sogou.com/reventondc/translateV*']
let isFirst = false
let onBeforeRequest = function (details) {
if (isFirst) return
isFirst = true
let data = details.requestBody.formData
// console.log(data)
let url = details.url
setTimeout(() => {
post(url, data)
}, 200)
return {cancel: true}
}
onBeforeRequestAddListener(onBeforeRequest, {urls: urls})
// 请求接口数据修改
let onBeforeSendHeaders = function (details) {
let h = details.requestHeaders
h.push({name: 'Host', value: 'fanyi.sogou.com'})
h.push({name: 'Origin', value: 'https://fanyi.sogou.com'})
h.push({name: 'Referer', value: url})
h.push({name: 'Sec-Fetch-Site', value: 'same-origin'})
return {requestHeaders: h}
}
// 销毁
let removeListener = function () {
// el.remove()
onHeadersReceivedRemoveListener(onRemoveFrame)
onBeforeSendHeadersRemoveListener(onBeforeSendHeaders)
onBeforeRequestRemoveListener(onBeforeRequest)
}
// 获取数据
let post = (url, data) => {
onBeforeSendHeadersAddListener(onBeforeSendHeaders, {urls: urls})
let p = new URLSearchParams(data)
httpPost({url: url, body: p.toString()}).then(r => {
removeListener()
if (r) {
resolve(this.unify(r, q, srcLan, tarLan))
} else {
reject('sogou trans error!')
}
}).catch(e => {
removeListener()
reject(e)
})
}
})
},*/
trans(q, srcLan, tarLan) {
srcLan = this.langMap[srcLan] || 'auto'
tarLan = this.langMap[tarLan] || 'zh-CHS'
return new Promise((resolve, reject) => {
if (q.length > 5000) return reject('The text is too large!')
let url = `https://fanyi.sogou.com/?keyword=${encodeURI(q)}&transfrom=${srcLan}&transto=${tarLan}&model=general`
let pageId = 'fy_soGou'
openIframe(pageId, url, 60 * 1000)
httpGet(url, 'document').then(r => {
// 获取翻译结果
let data
let sEl = r.querySelectorAll('script')
for (let i = 0; i < sEl.length; i++) {
let el = sEl[i]
if (el.getAttribute('src')) continue
let s = el.textContent
if (!s) continue
let arr = s.match(/window\.__INITIAL_STATE__=(.*?);\(function\(\){var s;/m)
if (!arr || arr.length < 2) continue
try {
data = JSON.parse(arr[1])
if (data) break
} catch (e) {
debug('json error!')
return reject('JSON.parse Error!')
}
}
if (data) {
resolve(this.unify(data, q, srcLan, tarLan))
} else {
reject('Get data is empty!')
}
}).catch(e => {
reject(e)
})
})
},
unify(r, text, srcLan, tarLan) {
// console.log('sogou:', r, text, srcLan, tarLan)
// console.log(JSON.stringify(r))
// 修正改版 2021.1.8
if (srcLan === 'auto') {
let str = getJSONValue(r, 'textTranslate.translateData.detect.detect')
if (str && isString(str)) srcLan = str
}
let map = this.langMapInvert
srcLan = map[srcLan] || 'auto'
tarLan = map[tarLan] || ''
let data = []
let tar = getJSONValue(r, 'textTranslate.result')
if (tar) {
let srcArr = text.split('\n')
let tarArr = tar.split('\n')
tarArr.forEach((tar, key) => {
if (tar) data.push({srcText: srcArr[key] || '', tarText: tar})
})
}
if (setting.translateThin) return {text, srcLan, tarLan, lanTTS: null, data} // 精简显示
// 重点词汇
let s = ''
let keywords = getJSONValue(r, 'textTranslate.translateData.keywords')
if (keywords && keywords.length > 0) {
s += `
重点词汇
`
s += `
`
keywords.forEach(v => {
if (v.key && v.value) s += `
${v.key}${v.value}
`
})
s += `
`
}
// 音标
let phonetic = getJSONValue(r, 'textTranslate.translateData.voice.phonetic')
let phStr = ''
if (phonetic && phonetic.length > 0) {
let getIconHTML = function (type, filename) {
if (type !== 'uk') type = 'us'
let title = type === 'uk' ? '英音' : '美音'
filename = (filename.substring(0, 2) === '//' ? 'https:' : 'https://fanyi.sogou.com') + filename
return ``
}
let ph_uk = '', ph_us = '', ph_mp3 = ''
phonetic.forEach(v => {
if (!v.text || !v.type) return
if (!v.filename) {
v.filename = `/reventondc/synthesis?text=${encodeURI(text)}&speed=1&lang=${srcLan}&from=translateweb`
}
if (v.type === 'uk') ph_uk = v.text
if (v.type === 'usa') ph_us = v.text
ph_mp3 += getIconHTML(v.type, v.filename)
})
if (ph_uk && ph_mp3) phStr += `
================================================
FILE: tool/google.html
================================================
Title
================================================
FILE: tool/lang.html
================================================
Title
语音转文本
语言Language
区域设置 (BCP-47) Locale (BCP-47)
自定义Customizations
阿拉伯语(巴林),现代标准Arabic (Bahrain), modern standard
ar-BH
语言模型Language model
阿拉伯语(埃及)Arabic (Egypt)
ar-EG
语言模型Language model
阿拉伯语(伊拉克)Arabic (Iraq)
ar-IQ
语言模型Language model
阿拉伯语(约旦)Arabic (Jordan)
ar-JO
语言模型Language model
阿拉伯语(科威特)Arabic (Kuwait)
ar-KW
语言模型Language model
阿拉伯语(黎巴嫩)Arabic (Lebanon)
ar-LB
语言模型Language model
阿拉伯语(阿曼)Arabic (Oman)
ar-OM
语言模型Language model
阿拉伯语(卡塔尔)Arabic (Qatar)
ar-QA
语言模型Language model
阿拉伯语(沙特阿拉伯)Arabic (Saudi Arabia)
ar-SA
语言模型Language model
阿拉伯语(叙利亚)Arabic (Syria)
ar-SY
语言模型Language model
阿拉伯语(阿拉伯联合酋长国)Arabic (United Arab Emirates)
ar-AE
语言模型Language model
保加利亚语(保加利亚)Bulgarian (Bulgaria)
bg-BG
语言模型Language model
加泰罗尼亚语(西班牙)Catalan (Spain)
ca-ES
语言模型Language model
中文(粤语,繁体)Chinese (Cantonese, Traditional)
zh-HK
语言模型Language model
中文(普通话,简体)Chinese (Mandarin, Simplified)
zh-CN
声学模型Acoustic model 语言模型Language model
中文(台湾普通话)Chinese (Taiwanese Mandarin)
zh-TW
语言模型Language model
克罗地亚语(克罗地亚)Croatian (Croatia)
hr-HR
语言模型Language model
捷克语(捷克共和国)Czech (Czech Republic)
cs-CZ
语言模型Language Model
丹麦语(丹麦)Danish (Denmark)
da-DK
语言模型Language model
荷兰语(荷兰)Dutch (Netherlands)
nl-NL
语言模型Language model
英语(澳大利亚)English (Australia)
en-AU
声学模型Acoustic model 语言模型Language model
英语(加拿大)English (Canada)
en-CA
声学模型Acoustic model 语言模型Language model
英语(香港)English (Hong Kong)
en-HK
语言模型Language Model
英语(印度)English (India)
en-IN
声学模型Acoustic model 语言模型Language model
英语(爱尔兰)English (Ireland)
en-IE
语言模型Language Model
英语(新西兰)English (New Zealand)
en-NZ
声学模型Acoustic model 语言模型Language model
英语(菲律宾)English (Philippines)
en-PH
语言模型Language Model
英语(新加坡)English (Singapore)
en-SG
语言模型Language Model
英语(南非)English (South Africa)
en-ZA
语言模型Language Model
英语(英国)English (United Kingdom)
en-GB
声学模型Acoustic model 语言模型Language model 发音Pronunciation
英语(美国)English (United States)
en-US
声学模型Acoustic model 语言模型Language model 发音Pronunciation
爱沙尼亚语(爱沙尼亚)Estonian(Estonia)
et-EE
语言模型Language Model
芬兰语(芬兰)Finnish (Finland)
fi-FI
语言模型Language model
法语(加拿大)French (Canada)
fr-CA
声学模型Acoustic model 语言模型Language model
法语(法国)French (France)
fr-FR
声学模型Acoustic model 语言模型Language model 发音Pronunciation
德语(德国)German (Germany)
de-DE
声学模型Acoustic model 语言模型Language model 发音Pronunciation
希腊语(希腊)Greek (Greece)
el-GR
语言模型Language model
古吉拉特语(印度)Gujarati (Indian)
gu-IN
语言模型Language model
印地语(印度)Hindi (India)
hi-IN
声学模型Acoustic model 语言模型Language model
匈牙利语(匈牙利)Hungarian (Hungary)
hu-HU
语言模型Language Model
爱尔兰语(爱尔兰)Irish(Ireland)
ga-IE
语言模型Language model
意大利语(意大利)Italian (Italy)
it-IT
声学模型Acoustic model 语言模型Language model 发音Pronunciation
日语(日本)Japanese (Japan)
ja-JP
语言模型Language model
韩语(韩国)Korean (Korea)
ko-KR
语言模型Language model
拉脱维亚语(拉脱维亚)Latvian (Latvia)
lv-LV
语言模型Language model
立陶宛语(立陶宛)Lithuanian (Lithuania)
lt-LT
语言模型Language model
马耳他语(马耳他)Maltese(Malta)
mt-MT
语言模型Language model
马拉地语(印度)Marathi (India)
mr-IN
语言模型Language model
挪威语(博克马尔语,挪威)Norwegian (Bokmål, Norway)
nb-NO
语言模型Language model
波兰语(波兰)Polish (Poland)
pl-PL
语言模型Language model
葡萄牙语(巴西)Portuguese (Brazil)
pt-BR
声学模型Acoustic model 语言模型Language model 发音Pronunciation
================================================
FILE: tool/qq.html
================================================
Title
================================================
FILE: tool/sogou.html
================================================
Title
================================================
FILE: tool/youdao.html
================================================
Title