Repository: ryanker/dream_translate Branch: main Commit: 34a941fb0578 Files: 76 Total size: 602.5 KB Directory structure: gitextract_au70of8y/ ├── LICENSE ├── README.md ├── release.sh ├── src/ │ ├── conf/ │ │ ├── conf.json │ │ ├── langSpeak.json │ │ ├── language.json │ │ └── searchText.txt │ ├── css/ │ │ ├── content.css │ │ ├── dmx_dialog.css │ │ ├── longman.css │ │ ├── main.css │ │ └── popup.css │ ├── html/ │ │ ├── favorite.html │ │ ├── history.html │ │ ├── more.html │ │ ├── popup.html │ │ ├── record.html │ │ ├── setting.html │ │ ├── speak.html │ │ └── video.html │ ├── js/ │ │ ├── background.js │ │ ├── common.js │ │ ├── content.js │ │ ├── db.js │ │ ├── dictionary/ │ │ │ ├── bing.js │ │ │ ├── cambridge.js │ │ │ ├── collins.js │ │ │ ├── dictcn.js │ │ │ ├── dictionary.js │ │ │ ├── dreye.js │ │ │ ├── etymonline.js │ │ │ ├── eudic.js │ │ │ ├── hjdict.js │ │ │ ├── iciba.js │ │ │ ├── lexico.js │ │ │ ├── longman.js │ │ │ ├── macmillan.js │ │ │ ├── merriam.js │ │ │ ├── oxford.js │ │ │ ├── rrdict.js │ │ │ ├── thefree.js │ │ │ ├── urban.js │ │ │ ├── vocabulary.js │ │ │ ├── wordreference.js │ │ │ └── youdao.js │ │ ├── favorite.js │ │ ├── frame.js │ │ ├── history.js │ │ ├── more.js │ │ ├── player.js │ │ ├── popup.js │ │ ├── record.js │ │ ├── setting.js │ │ ├── speak.js │ │ ├── translate/ │ │ │ ├── alibaba.js │ │ │ ├── baidu.js │ │ │ ├── bing.js │ │ │ ├── deepl.js │ │ │ ├── frdic.js │ │ │ ├── google.js │ │ │ ├── local.js │ │ │ ├── qq.js │ │ │ ├── so.js │ │ │ ├── sogou.js │ │ │ └── youdao.js │ │ └── video.js │ └── manifest.json └── tool/ ├── alibaba.html ├── baidu.html ├── bd.js ├── bing.html ├── google.html ├── lang.html ├── qq.html ├── sogou.html └── youdao.html ================================================ FILE CONTENTS ================================================ ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2020 Ryan Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # 梦想划词翻译(Chrome & Firefox 扩展程序) 这是一款精心雕琢并免费开源的划词翻译扩展,也是一款致力于改善「中国式哑巴英语」而设计的一款英语练习扩展程序。 ### 扩展安装 - Chrome 安装:[梦想划词翻译—聚合词典搜索 - Chrome 网上应用店](https://chrome.google.com/webstore/detail/odfgigmpkhhhieicogijogfobfipijjh) - Edge 安装:[梦想划词翻译—聚合词典搜索 - Microsoft Edge 扩展商店](https://microsoftedge.microsoft.com/addons/detail/ogleeipbbcokpeabliinildngejmdneg) - Firefox 安装:[梦想划词翻译—聚合词典搜索 – Firefox 扩展商店](https://addons.mozilla.org/zh-CN/firefox/addon/dream_translate/) ### 程序特点 考虑到程序的维护时间成本,本程序采用纯原生 javascript 开发,拥有更好的性能,同时占用内存更小,没有对各种技术栈的依赖,减少了 bug 的产生和维护时间的投入。 自主设计的轻量级翻译查词对话框,占用内存小,且可拖动大小,移动位置,固定和全屏窗口,使用起来简单便捷,顺心应手。 自主设计了大量字体图标,零图片使用,不仅减少了 IO 请求,内存占用也更低,还降低了程序业务逻辑和维护成本。 程序代码短小精干,没有那么多花里胡哨的功能;也没有像剥洋葱一样的代码,读源码不会丈二和尚摸不着头脑。 ### 快捷键 目前支持 6 个快捷键,为便于记忆,默认预设置 4 个快捷键。 划词翻译开关 Ctrl+Shift+X 「常用,很多时候都不需要开启划词翻译,这时关闭就行;类似"剪切"快捷键,记忆技巧:剪一下开,剪一下关,剪一下开啊,剪一下关。」 打开翻译窗口 Ctrl+Shift+9 「较常用,这是一个全局快捷键,在使用任何软件时,都可以通过这个快捷键召唤翻译小窗口;记忆技巧:久的谐音,长长久久。」(目前 Firefox 不支持全局快捷键) 截图识别翻译 Ctrl+Shift+A 「较常用,对页面局部切图后,识别图片内容,然后进行翻译。」 打开翻译面板 Alt+D 「不常用,一般通过鼠标点击就可以了,但也提供给喜欢使用快捷键人群一个方便;记忆技巧:字母 D 是 Dream 首字母,梦想成真!」 剪贴板内容翻译 「需要自定义启用,从剪贴板获取文本内容进行翻译。」 停止播放声音 「需要自定义启用,有时朗读声音时,想要停止播放,可以通过此快捷键,快速停止播放声音。」 最后,如果这些快捷键和你电脑快捷键冲突了,那就只能自定义符合自己习惯的快捷键了。^_^ ## 聚合的词典&翻译 ### 在线翻译(国内) 百度翻译 https://fanyi.baidu.com 谷歌翻译 https://translate.google.cn 必应翻译 https://cn.bing.com/translator 有道翻译 https://fanyi.youdao.com 腾讯翻译 https://fanyi.qq.com 搜狗翻译 https://fanyi.sogou.com 阿里翻译 https://translate.alibaba.com 360翻译 https://fanyi.so.com ### 在线翻译(国外) DeepL翻译 https://www.deepl.com/translator (维基百科:专家认为它比 Google 翻译更准确自然) ### 在线词典(国内) 有道词典 https://www.youdao.com 必应词典 https://cn.bing.com/dict 海词词典 https://dict.cn 金山词典 https://www.iciba.com 欧路词典 https://dict.eudic.net 人人词典 https://www.91dict.com 沪江词典 https://www.hjdict.com 译典词典 https://www.dreye.com.cn ### 在线词典(国外) 朗文词典 https://www.ldoceonline.com 剑桥词典 https://dictionary.cambridge.org 韦氏词典 https://www.merriam-webster.com 柯林斯词典 https://www.collinsdictionary.com 牛津词典 https://www.oxfordlearnersdictionaries.com 自由词典 https://www.thefreedictionary.com 词曲词典 https://www.lexico.com 在线词典 https://www.dictionary.com 词汇词典 https://www.vocabulary.com 语言词典 https://www.wordreference.com 词源词典 https://www.etymonline.com 城市词典 https://www.urbandictionary.com 麦克米伦词典 https://www.macmillandictionary.com ================================================ FILE: release.sh ================================================ #!/bin/bash DIST=./dist DIST2=./dist_firefox # ======== Chrome & Edge ======== # 拷贝 echo "cp to ... $DIST" mkdir -p $DIST \cp -af ./src/ $DIST # 打包 (Chrome Edge) sed -i "" -e "s/const isDebug = true/const isDebug = false/g" $DIST/js/common.js zip -rq dist.zip $DIST # ======== Firefox ======== # 拷贝 echo "cp to ... $DIST2" mkdir -p $DIST2 \cp -af ./src/ $DIST2 # 替换 sed -i "" -e "s/const isDebug = true/const isDebug = false/g" $DIST2/js/common.js sed -i "" -e '16,21d' $DIST2/css/content.css # 清理 CSS 样式 sed -i "" -e '/"tts",/d' $DIST2/manifest.json sed -i "" -e '/"global": true,/d' $DIST2/manifest.json # 打包 (Firefox) zip -rq dist_firefox.zip $DIST2 # 清理目录 rm -rf $DIST rm -rf $DIST2 ================================================ FILE: src/conf/conf.json ================================================ { "setting": { "scribble": "direct", "excludeChinese": "", "excludeSymbol": "", "excludeNumber": "", "position": "fixed", "allowSelect": "", "autoCopy": "", "autoPaste": "", "autoChange": "", "autoWords": "", "cutHumpName": "on", "translateList": [ "baidu", "google", "youdao" ], "translateTTSList": [], "localSoundReplace": "", "translateOCR": "", "ocrType": "auto", "translateThin": "", "hideOriginal": "", "autoLanguage": "on", "autoConfirm": "on", "dictionaryList": [ "youdao", "bing", "longman" ], "dictionarySoundList": [], "dictionaryReader": "us", "searchList": [ "百度搜索", "谷歌搜索", "必应搜索", "360搜索", "搜狗搜索", "谷歌图片", "必应图片", "百度图片", "维基百科", "百度百科" ], "searchMenus": [], "searchSide": [], "ttsConf": {} }, "translateList": { "baidu": "百度翻译", "google": "谷歌翻译", "youdao": "有道翻译", "bing": "必应翻译", "qq": "腾讯翻译", "alibaba": "阿里翻译", "sogou": "搜狗翻译", "so": "360翻译", "deepl": "DeepL翻译" }, "translateTTSList": { "baidu": "百度朗读", "google": "谷歌朗读", "youdao": "有道朗读", "bing": "必应朗读", "qq": "腾讯朗读", "sogou": "搜狗朗读", "frdic": "欧路朗读", "local": "本机朗读" }, "dictionaryList": { "youdao": "有道词典", "bing": "必应词典", "dictcn": "海词词典", "iciba": "金山词典", "eudic": "欧路词典", "rrdict": "人人词典", "hjdict": "沪江词典", "dreye": "译典词典", "longman": "朗文词典", "cambridge": "剑桥词典", "merriam": "韦氏词典", "collins": "柯林斯词典", "oxford": "牛津词典", "thefree": "自由词典", "lexico": "词曲词典", "dictionary": "在线词典", "vocabulary": "词汇词典", "wordreference": "语言词典", "etymonline": "词源词典", "urban": "城市词典", "macmillan": "麦克米伦词典" }, "dictionaryCSS": [ "longman" ], "dictionarySoundExcluded": [ "etymonline", "urban" ], "ttsList": { "ara": "ar-SA", "cs": "cs-CZ", "dan": "da-DK", "de": "de-DE", "el": "el-GR", "en": "en-US", "spa": "es-ES", "fin": "fi-FI", "frn": "fr-CA", "fra": "fr-FR", "heb": "he-IL", "hi": "hi-IN", "hu": "hu-HU", "id": "id-ID", "it": "it-IT", "jp": "ja-JP", "kor": "ko-KR", "nor": "nb-NO", "nl": "nl-NL", "pl": "pl-PL", "pot": "pt-BR", "pt": "pt-PT", "rom": "ro-RO", "ru": "ru-RU", "sk": "sk-SK", "swe": "sv-SE", "th": "th-TH", "tr": "tr-TR", "zh": "zh-CN", "yue": "zh-HK", "cht": "zh-TW" }, "dialogConf": { "width": 500, "height": 470, "source": "auto", "target": "zh", "action": "translate" } } ================================================ FILE: src/conf/langSpeak.json ================================================ { "ar-AE": { "zhName": "阿拉伯语(阿拉伯联合酋长国)", "enName": "Arabic(United Arab Emirates)" }, "ar-BH": { "zhName": "阿拉伯语(巴林)", "enName": "Arabic(Bahrain)" }, "ar-EG": { "zhName": "阿拉伯语(埃及)", "enName": "Arabic(Egypt)" }, "ar-IQ": { "zhName": "阿拉伯语(伊拉克)", "enName": "Arabic(Iraq)" }, "ar-JO": { "zhName": "阿拉伯语(约旦)", "enName": "Arabic(Jordan)" }, "ar-KW": { "zhName": "阿拉伯语(科威特)", "enName": "Arabic(Kuwait)" }, "ar-LB": { "zhName": "阿拉伯语(黎巴嫩)", "enName": "Arabic(Lebanon)" }, "ar-OM": { "zhName": "阿拉伯语(阿曼)", "enName": "Arabic(Oman)" }, "ar-QA": { "zhName": "阿拉伯语(卡塔尔)", "enName": "Arabic(Qatar)" }, "ar-SA": { "zhName": "阿拉伯语(沙特阿拉伯)", "enName": "Arabic(Saudi Arabia)" }, "ar-SY": { "zhName": "阿拉伯语(叙利亚)", "enName": "Arabic(Syria)" }, "bg-BG": { "zhName": "保加利亚语(保加利亚)", "enName": "Bulgarian(Bulgaria)" }, "ca-ES": { "zhName": "加泰罗尼亚语(西班牙)", "enName": "Catalan(Spain)" }, "cs-CZ": { "zhName": "捷克语(捷克共和国)", "enName": "Czech(Czech Republic)" }, "da-DK": { "zhName": "丹麦语(丹麦)", "enName": "Danish(Denmark)" }, "de-AT": { "zhName": "德语(奥地利)", "enName": "German(Austria)" }, "de-CH": { "zhName": "德语(瑞士)", "enName": "German(Switzerland)" }, "de-DE": { "zhName": "德语(德国)", "enName": "German(Germany)" }, "el-GR": { "zhName": "希腊语(希腊)", "enName": "Greek(Greece)" }, "en": { "zhName": "英语", "enName": "English" }, "en-AU": { "zhName": "英语(澳大利亚)", "enName": "English(Australia)" }, "en-CA": { "zhName": "英语(加拿大)", "enName": "English(Canada)" }, "en-GB": { "zhName": "英语(英国)", "enName": "English(United Kingdom)" }, "en-HK": { "zhName": "英语(香港)", "enName": "English(Hong Kong)" }, "en-IE": { "zhName": "英语(爱尔兰)", "enName": "English(Ireland)" }, "en-IN": { "zhName": "英语(印度)", "enName": "English(India)" }, "en-NZ": { "zhName": "英语(新西兰)", "enName": "English(New Zealand)" }, "en-PH": { "zhName": "英语(菲律宾)", "enName": "English(Philippines)" }, "en-SG": { "zhName": "英语(新加坡)", "enName": "English(Singapore)" }, "en-US": { "zhName": "英语(美国)", "enName": "English(United States)" }, "en-ZA": { "zhName": "英语(南非)", "enName": "English(South Africa)" }, "es": { "zhName": "西班牙语", "enName": "Spanish" }, "es-AR": { "zhName": "西班牙语(阿根廷)", "enName": "Spanish(Argentina)" }, "es-BO": { "zhName": "西班牙语(玻利维亚)", "enName": "Spanish(Bolivia)" }, "es-CL": { "zhName": "西班牙语(智利)", "enName": "Spanish(Chile)" }, "es-CO": { "zhName": "西班牙语(哥伦比亚)", "enName": "Spanish(Colombia)" }, "es-CR": { "zhName": "西班牙语(哥斯达黎加)", "enName": "Spanish(Costa Rica)" }, "es-CU": { "zhName": "西班牙语(古巴)", "enName": "Spanish(Cuba)" }, "es-DO": { "zhName": "西班牙语(多米尼加共和国)", "enName": "Spanish(Dominican Republic)" }, "es-EC": { "zhName": "西班牙语(厄瓜多尔)", "enName": "Spanish(Ecuador)" }, "es-ES": { "zhName": "西班牙语(西班牙)", "enName": "Spanish(Spain)" }, "es-GT": { "zhName": "西班牙语(危地马拉)", "enName": "Spanish(Guatemala)" }, "es-HN": { "zhName": "西班牙语(洪都拉斯)", "enName": "Spanish(Honduras)" }, "es-MX": { "zhName": "西班牙语(墨西哥)", "enName": "Spanish(Mexico)" }, "es-NI": { "zhName": "西班牙语(尼加拉瓜)", "enName": "Spanish(Nicaragua)" }, "es-PA": { "zhName": "西班牙语(巴拿马)", "enName": "Spanish(Panama)" }, "es-PE": { "zhName": "西班牙语(秘鲁)", "enName": "Spanish(Peru)" }, "es-PR": { "zhName": "西班牙语(波多黎各)", "enName": "Spanish(Puerto Rico)" }, "es-PY": { "zhName": "西班牙语(巴拉圭)", "enName": "Spanish(Paraguay)" }, "es-SV": { "zhName": "西班牙语(萨尔瓦多)", "enName": "Spanish(El Salvador)" }, "es-US": { "zhName": "西班牙语(美国)", "enName": "Spanish(USA)" }, "es-UY": { "zhName": "西班牙语(乌拉圭)", "enName": "Spanish(Uruguay)" }, "es-VE": { "zhName": "西班牙语(委内瑞拉)", "enName": "Spanish(Venezuela)" }, "et-EE": { "zhName": "爱沙尼亚语(爱沙尼亚)", "enName": "Estonian(Estonia)" }, "fi-FI": { "zhName": "芬兰语(芬兰)", "enName": "Finnish(Finland)" }, "fr-CA": { "zhName": "法语(加拿大)", "enName": "French(Canada)" }, "fr-CH": { "zhName": "法语(瑞士)", "enName": "French(Switzerland)" }, "fr-FR": { "zhName": "法语(法国)", "enName": "French(France)" }, "ga-IE": { "zhName": "爱尔兰语(爱尔兰)", "enName": "Irish(Ireland)" }, "gu-IN": { "zhName": "古吉拉特语(印度)", "enName": "Gujarati(Indian)" }, "he-IL": { "zhName": "希伯来语(以色列)", "enName": "Hebrew(Israel)" }, "hi-IN": { "zhName": "印地语(印度)", "enName": "Hindi(India)" }, "hr-HR": { "zhName": "克罗地亚语(克罗地亚)", "enName": "Croatian(Croatia)" }, "hu-HU": { "zhName": "匈牙利语(匈牙利)", "enName": "Hungarian(Hungary)" }, "id-ID": { "zhName": "印度尼西亚语(印度尼西亚)", "enName": "Indonesian(Indonesia)" }, "it-IT": { "zhName": "意大利语(意大利)", "enName": "Italian(Italy)" }, "ja-JP": { "zhName": "日语(日本)", "enName": "Japanese(Japan)" }, "ko-KR": { "zhName": "韩语(韩国)", "enName": "Korean(Korea)" }, "lt-LT": { "zhName": "立陶宛语(立陶宛)", "enName": "Lithuanian(Lithuania)" }, "lv-LV": { "zhName": "拉脱维亚语(拉脱维亚)", "enName": "Latvian(Latvia)" }, "mr-IN": { "zhName": "马拉地语(印度)", "enName": "Marathi(India)" }, "ms-MY": { "zhName": "马来语(马来西亚)", "enName": "Malay(Malaysia)" }, "mt-MT": { "zhName": "马耳他语(马耳他)", "enName": "Maltese(Malta)" }, "nb-NO": { "zhName": "挪威语(博克马尔语,挪威)", "enName": "Norwegian(Bokmål, Norway)" }, "nl": { "zhName": "荷兰语", "enName": "Dutch" }, "nl-BE": { "zhName": "荷兰语(比利时)", "enName": "Dutch(Belgium)" }, "nl-NL": { "zhName": "荷兰语(荷兰)", "enName": "Dutch(Netherlands)" }, "pl-PL": { "zhName": "波兰语(波兰)", "enName": "Polish(Poland)" }, "pt-BR": { "zhName": "葡萄牙语(巴西)", "enName": "Portuguese(Brazil)" }, "pt-PT": { "zhName": "葡萄牙语(葡萄牙)", "enName": "Portuguese(Portugal)" }, "ro-RO": { "zhName": "罗马尼亚语(罗马尼亚)", "enName": "Romanian(Romania)" }, "ru-RU": { "zhName": "俄语(俄罗斯)", "enName": "Russian(Russia)" }, "sk-SK": { "zhName": "斯洛伐克语(斯洛伐克)", "enName": "Slovak(Slovakia)" }, "sl-SI": { "zhName": "斯洛文尼亚语(斯洛文尼亚)", "enName": "Slovenian(Slovenia)" }, "sv-SE": { "zhName": "瑞典语(瑞典)", "enName": "Swedish(Sweden)" }, "ta-IN": { "zhName": "泰米尔语(印度)", "enName": "Tamil(India)" }, "te-IN": { "zhName": "泰卢固语(印度)", "enName": "Telugu(India)" }, "th-TH": { "zhName": "泰语(泰国)", "enName": "Thai(Thailand)" }, "tr-TR": { "zhName": "土耳其语(土耳其)", "enName": "Turkish(Turkey)" }, "vi-VN": { "zhName": "越南语(越南)", "enName": "Vietnamese(Vietnam)" }, "zh-CN": { "zhName": "中文(简体)", "enName": "Chinese(Simplified)" }, "zh-HK": { "zhName": "中文(粤语)", "enName": "Chinese(Cantonese)" }, "zh-TW": { "zhName": "中文(繁体)", "enName": "Chinese(Traditional)" } } ================================================ FILE: src/conf/language.json ================================================ { "auto": { "zhName": "自动检测", "enName": "Auto Detect" }, "zh": { "zhName": "中文(简体)", "enName": "Chinese" }, "en": { "zhName": "英语", "enName": "English" }, "jp": { "zhName": "日语", "enName": "Japanese" }, "th": { "zhName": "泰语", "enName": "Thai" }, "spa": { "zhName": "西班牙语", "enName": "Spanish" }, "ara": { "zhName": "阿拉伯语", "enName": "Arabic" }, "fra": { "zhName": "法语", "enName": "French" }, "kor": { "zhName": "韩语", "enName": "Korean" }, "ru": { "zhName": "俄语", "enName": "Russian" }, "de": { "zhName": "德语", "enName": "German" }, "pt": { "zhName": "葡萄牙语", "enName": "Portuguese" }, "it": { "zhName": "意大利语", "enName": "Italian" }, "el": { "zhName": "希腊语", "enName": "Greek" }, "nl": { "zhName": "荷兰语", "enName": "Dutch" }, "pl": { "zhName": "波兰语", "enName": "Polish" }, "fin": { "zhName": "芬兰语", "enName": "Finnish" }, "cs": { "zhName": "捷克语", "enName": "Czech" }, "bul": { "zhName": "保加利亚语", "enName": "Bulgarian" }, "dan": { "zhName": "丹麦语", "enName": "Danish" }, "est": { "zhName": "爱沙尼亚语", "enName": "Estonian" }, "hu": { "zhName": "匈牙利语", "enName": "Hungarian" }, "rom": { "zhName": "罗马尼亚语", "enName": "Romanian" }, "slo": { "zhName": "斯洛文尼亚语", "enName": "Slovenian" }, "swe": { "zhName": "瑞典语", "enName": "Swedish" }, "vie": { "zhName": "越南语", "enName": "Vietnamese" }, "yue": { "zhName": "中文(粤语)", "enName": "Cantonese" }, "cht": { "zhName": "中文(繁体)", "enName": "Traditional Chinese" }, "wyw": { "zhName": "中文(文言文)", "enName": "Classical Chinese" }, "afr": { "zhName": "南非荷兰语", "enName": "Afrikaans" }, "alb": { "zhName": "阿尔巴尼亚语", "enName": "Albanian" }, "amh": { "zhName": "阿姆哈拉语", "enName": "Amharic" }, "arm": { "zhName": "亚美尼亚语", "enName": "Armenian" }, "asm": { "zhName": "阿萨姆语", "enName": "Assamese" }, "ast": { "zhName": "阿斯图里亚斯语", "enName": "Asturian" }, "aze": { "zhName": "阿塞拜疆语", "enName": "Azerbaijani" }, "baq": { "zhName": "巴斯克语", "enName": "Basque" }, "bel": { "zhName": "白俄罗斯语", "enName": "Belarusian" }, "ben": { "zhName": "孟加拉语", "enName": "Bengali" }, "bos": { "zhName": "波斯尼亚语", "enName": "Bosnian" }, "bur": { "zhName": "缅甸语", "enName": "Burmese" }, "cat": { "zhName": "加泰罗尼亚语", "enName": "Catalan" }, "ceb": { "zhName": "宿务语", "enName": "Cebuano" }, "hrv": { "zhName": "克罗地亚语", "enName": "Croatian" }, "epo": { "zhName": "世界语", "enName": "Esperanto" }, "fao": { "zhName": "法罗语", "enName": "Faroese" }, "fil": { "zhName": "菲律宾语", "enName": "Filipino" }, "glg": { "zhName": "加利西亚语", "enName": "Galician" }, "geo": { "zhName": "格鲁吉亚语", "enName": "Georgian" }, "guj": { "zhName": "古吉拉特语", "enName": "Gujarati" }, "hau": { "zhName": "豪萨语", "enName": "Hausa" }, "heb": { "zhName": "希伯来语", "enName": "Hebrew" }, "hi": { "zhName": "印地语", "enName": "Hindi" }, "ice": { "zhName": "冰岛语", "enName": "Icelandic" }, "ibo": { "zhName": "伊博语", "enName": "Igbo" }, "id": { "zhName": "印尼语", "enName": "Indonesian" }, "gle": { "zhName": "爱尔兰语", "enName": "Irish" }, "kan": { "zhName": "卡纳达语", "enName": "Kannada" }, "kaz": { "zhName": "哈萨克语", "enName": "Kazakh" }, "kli": { "zhName": "克林贡语", "enName": "Klingon" }, "kur": { "zhName": "库尔德语", "enName": "Kurdish" }, "lao": { "zhName": "老挝语", "enName": "Lao" }, "lat": { "zhName": "拉丁语", "enName": "Latin" }, "lav": { "zhName": "拉脱维亚语", "enName": "Latvian" }, "lit": { "zhName": "立陶宛语", "enName": "Lithuanian" }, "ltz": { "zhName": "卢森堡语", "enName": "Luxembourgish" }, "mac": { "zhName": "马其顿语", "enName": "Macedonian" }, "mg": { "zhName": "马拉加斯语", "enName": "Malagasy" }, "may": { "zhName": "马来语", "enName": "Malay" }, "mal": { "zhName": "马拉雅拉姆语", "enName": "Malayalam" }, "mlt": { "zhName": "马耳他语", "enName": "Maltese" }, "mar": { "zhName": "马拉地语", "enName": "Marathi" }, "nep": { "zhName": "尼泊尔语", "enName": "Nepali" }, "nno": { "zhName": "新挪威语", "enName": "Nynorsk" }, "per": { "zhName": "波斯语", "enName": "Persian" }, "srd": { "zhName": "萨丁尼亚语", "enName": "Sardinian" }, "srp": { "zhName": "塞尔维亚语(拉丁文)", "enName": "Serbian" }, "sin": { "zhName": "僧伽罗语", "enName": "Sinhala" }, "sk": { "zhName": "斯洛伐克语", "enName": "Slovak" }, "som": { "zhName": "索马里语", "enName": "Somali" }, "swa": { "zhName": "斯瓦希里语", "enName": "Swahili" }, "tgl": { "zhName": "他加禄语", "enName": "Tagalog" }, "tgk": { "zhName": "塔吉克语", "enName": "Tajik" }, "tam": { "zhName": "泰米尔语", "enName": "Tamil" }, "tat": { "zhName": "鞑靼语", "enName": "Tatar" }, "tel": { "zhName": "泰卢固语", "enName": "Telugu" }, "tr": { "zhName": "土耳其语", "enName": "Turkish" }, "tuk": { "zhName": "土库曼语", "enName": "Turkmen" }, "ukr": { "zhName": "乌克兰语", "enName": "Ukrainian" }, "urd": { "zhName": "乌尔都语", "enName": "Urdu" }, "uzb": { "zhName": "乌兹别克语", "enName": "Uzbek" }, "oci": { "zhName": "奥克语", "enName": "Occitan" }, "kir": { "zhName": "吉尔吉斯语", "enName": "Kyrgyz" }, "pus": { "zhName": "普什图语", "enName": "Pashto" }, "hkm": { "zhName": "高棉语", "enName": "Khmer" }, "ht": { "zhName": "海地语", "enName": "Haitian Creole" }, "nob": { "zhName": "书面挪威语", "enName": "Bokmål" }, "pan": { "zhName": "旁遮普语", "enName": "Punjabi" }, "arq": { "zhName": "阿尔及利亚阿拉伯语", "enName": "Algerian Arabic" }, "bis": { "zhName": "比斯拉马语", "enName": "Bislama" }, "frn": { "zhName": "加拿大法语", "enName": "Canadian French" }, "hak": { "zhName": "哈卡钦语", "enName": "Hakha Chin" }, "hup": { "zhName": "胡帕语", "enName": "Hupa" }, "ing": { "zhName": "印古什语", "enName": "Ingush" }, "lag": { "zhName": "拉特加莱语", "enName": "Latgalian" }, "mau": { "zhName": "毛里求斯克里奥尔语", "enName": "Mauritian Creole" }, "mot": { "zhName": "黑山语", "enName": "Montenegrin" }, "pot": { "zhName": "巴西葡萄牙语", "enName": "Brazilian Portuguese" }, "ruy": { "zhName": "卢森尼亚语", "enName": "Rusyn" }, "sec": { "zhName": "塞尔维亚-克罗地亚语", "enName": "Serbo-Croatian" }, "sil": { "zhName": "西里西亚语", "enName": "Silesian" }, "tua": { "zhName": "突尼斯阿拉伯语", "enName": "Tunisian Arabic" }, "ach": { "zhName": "亚齐语", "enName": "Achinese" }, "aka": { "zhName": "阿肯语", "enName": "Akan" }, "arg": { "zhName": "阿拉贡语", "enName": "Aragonese" }, "aym": { "zhName": "艾马拉语", "enName": "Aymara" }, "bal": { "zhName": "俾路支语", "enName": "Baluchi" }, "bak": { "zhName": "巴什基尔语", "enName": "Bashkir" }, "bem": { "zhName": "本巴语", "enName": "Bemba" }, "ber": { "zhName": "柏柏尔语", "enName": "Berber languages" }, "bho": { "zhName": "博杰普尔语", "enName": "Bhojpuri" }, "bli": { "zhName": "比林语", "enName": "Blin" }, "bre": { "zhName": "布列塔尼语", "enName": "Breton" }, "chr": { "zhName": "切罗基语", "enName": "Cherokee" }, "nya": { "zhName": "齐切瓦语", "enName": "Chichewa" }, "chv": { "zhName": "楚瓦什语", "enName": "Chuvash" }, "cor": { "zhName": "康瓦尔语", "enName": "Cornish" }, "cos": { "zhName": "科西嘉语", "enName": "Corsican" }, "cre": { "zhName": "克里克语", "enName": "Creek" }, "cri": { "zhName": "克里米亚鞑靼语", "enName": "Crimean Tatar" }, "div": { "zhName": "迪维希语", "enName": "Divehi" }, "eno": { "zhName": "古英语", "enName": "Old English" }, "frm": { "zhName": "中古法语", "enName": "Middle French" }, "fri": { "zhName": "弗留利语", "enName": "Friulian" }, "ful": { "zhName": "富拉尼语", "enName": "Fulani" }, "gla": { "zhName": "盖尔语", "enName": "Gaelic" }, "lug": { "zhName": "卢干达语", "enName": "Luganda" }, "gra": { "zhName": "古希腊语", "enName": "Ancient Greek" }, "grn": { "zhName": "瓜拉尼语", "enName": "Guarani" }, "haw": { "zhName": "夏威夷语", "enName": "Hawaiian" }, "hil": { "zhName": "希利盖农语", "enName": "Hiligaynon" }, "ido": { "zhName": "伊多语", "enName": "Ido" }, "ina": { "zhName": "因特语", "enName": "Interlingua" }, "iku": { "zhName": "伊努克提图特语", "enName": "Inuktitut" }, "jav": { "zhName": "爪哇语", "enName": "Javanese" }, "kab": { "zhName": "卡拜尔语", "enName": "Kabyle" }, "kal": { "zhName": "格陵兰语", "enName": "Kalaallisut" }, "kau": { "zhName": "卡努里语", "enName": "Kanuri" }, "kas": { "zhName": "克什米尔语", "enName": "Kashmiri" }, "kah": { "zhName": "卡舒比语", "enName": "Kashubian" }, "kin": { "zhName": "卢旺达语", "enName": "Kinyarwanda" }, "kon": { "zhName": "刚果语", "enName": "Kongo" }, "kok": { "zhName": "孔卡尼语", "enName": "Konkani" }, "lim": { "zhName": "林堡语", "enName": "Limburgish" }, "lin": { "zhName": "林加拉语", "enName": "Lingala" }, "loj": { "zhName": "逻辑语", "enName": "Lojban" }, "log": { "zhName": "低地德语", "enName": "Low German" }, "los": { "zhName": "下索布语", "enName": "Lower Sorbian" }, "mai": { "zhName": "迈蒂利语", "enName": "Maithili" }, "glv": { "zhName": "曼克斯语", "enName": "Manx" }, "mao": { "zhName": "毛利语", "enName": "Maori" }, "mah": { "zhName": "马绍尔语", "enName": "Marshallese" }, "nbl": { "zhName": "南恩德贝莱语", "enName": "Southern Ndebele" }, "nea": { "zhName": "那不勒斯语", "enName": "Neapolitan" }, "nqo": { "zhName": "西非书面语", "enName": "N'Ko" }, "sme": { "zhName": "北方萨米语", "enName": "Northern Sami" }, "nor": { "zhName": "挪威语", "enName": "Norwegian" }, "oji": { "zhName": "奥杰布瓦语", "enName": "Ojibwa" }, "ori": { "zhName": "奥里亚语", "enName": "Oriya" }, "orm": { "zhName": "奥罗莫语", "enName": "Oromo" }, "oss": { "zhName": "奥塞梯语", "enName": "Ossetian" }, "pam": { "zhName": "邦板牙语", "enName": "Pampanga" }, "pap": { "zhName": "帕皮阿门托语", "enName": "Papiamento" }, "ped": { "zhName": "北索托语", "enName": "Northern Sotho" }, "que": { "zhName": "克丘亚语", "enName": "Quechua" }, "roh": { "zhName": "罗曼什语", "enName": "Romansh" }, "ro": { "zhName": "罗姆语", "enName": "Romany" }, "sm": { "zhName": "萨摩亚语", "enName": "Samoan" }, "san": { "zhName": "梵语", "enName": "Sanskrit" }, "sco": { "zhName": "苏格兰语", "enName": "Scots" }, "sha": { "zhName": "掸语", "enName": "Shan" }, "sna": { "zhName": "修纳语", "enName": "Shona" }, "snd": { "zhName": "信德语", "enName": "Sindhi" }, "sol": { "zhName": "桑海语", "enName": "Songhai languages" }, "sot": { "zhName": "南索托语", "enName": "Southern Sotho" }, "syr": { "zhName": "叙利亚语", "enName": "Syriac" }, "tet": { "zhName": "德顿语", "enName": "Tetum" }, "tir": { "zhName": "提格利尼亚语", "enName": "Tigrinya" }, "tso": { "zhName": "聪加语", "enName": "Tsonga" }, "twi": { "zhName": "契维语", "enName": "Twi" }, "ups": { "zhName": "高地索布语", "enName": "Upper Sorbian" }, "ven": { "zhName": "文达语", "enName": "Venda" }, "wln": { "zhName": "瓦隆语", "enName": "Walloon" }, "wel": { "zhName": "威尔士语", "enName": "Welsh" }, "fry": { "zhName": "西弗里斯语", "enName": "Western Frisian" }, "wol": { "zhName": "沃洛夫语", "enName": "Wolof" }, "xho": { "zhName": "科萨语", "enName": "Xhosa" }, "yid": { "zhName": "意第绪语", "enName": "Yiddish" }, "yor": { "zhName": "约鲁巴语", "enName": "Yoruba" }, "zaz": { "zhName": "扎扎其语", "enName": "Zaza" }, "zul": { "zhName": "祖鲁语", "enName": "Zulu" }, "sun": { "zhName": "巽他语", "enName": "BasaSunda" }, "hmn": { "zhName": "苗语", "enName": "Hmong" }, "src": { "zhName": "塞尔维亚语(西里尔文)", "enName": "Serb(Cyrillic)" } } ================================================ FILE: src/conf/searchText.txt ================================================ 百度搜索|https://www.baidu.com/s?wd={0} 谷歌搜索|https://www.google.com/search?q={0} 必应搜索|https://cn.bing.com/search?q={0} 360搜索|https://www.so.com/s?q={0} 搜狗搜索|https://www.sogou.com/web?query={0} 谷歌图片|https://www.google.com/search?q={0}&tbm=isch 必应图片|https://cn.bing.com/images/search?q={0}&form=BESBTB&first=1&tsc=ImageBasicHover&ensearch=1 百度图片|https://image.baidu.com/search/index?tn=baiduimage&fm=result&ie=utf-8&word={0} 维基百科|https://zh.wikipedia.org/w/index.php?search={0} 百度百科|https://baike.baidu.com/search/word?word={0} 必应词典|https://cn.bing.com/dict/search?q={0} 有道词典|https://www.youdao.com/w/eng/{0} 金山词典|https://www.iciba.com/word?w={0} 牛津词典|https://www.oxfordlearnersdictionaries.com/search/english/?q={0} 剑桥词典|https://dictionary.cambridge.org/dictionary/english-chinese-simplified/{0} 格林斯词典|https://www.collinsdictionary.com/search/?dictCode=english&q={0} 豆瓣读书|https://search.douban.com/book/subject_search?search_text={0} 京东搜索|https://search.jd.com/Search?keyword={0} 淘宝搜索|https://s.taobao.com/search?q={0} MDN搜索|https://developer.mozilla.org/zh-CN/search?q={0} StackOverflow|https://stackoverflow.com/search?q={0} ================================================ FILE: src/css/content.css ================================================ .dmx_unselectable { -webkit-user-select: none !important; -moz-user-select: none !important; -ms-user-select: none !important; user-select: none !important; } .dmx_overflow_hidden { overflow: hidden; } div[mx-name="dream-translation"] { all: initial; } @font-face { font-family: "dmx-icon"; /*src: url('dmx_icon.woff2') format('woff2');*/ src: url('chrome-extension://__MSG_@@extension_id__/css/dmx_icon.woff2') format('woff2'); } /* Firefox */ @-moz-document url-prefix() { @font-face { font-family: "dmx-icon"; src: url('moz-extension://__MSG_@@extension_id__/css/dmx_icon.woff2') format('woff2'); } } .dmx-icon { font-family: "dmx-icon", serif !important; font-size: 16px; font-style: normal; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; user-select: none; } .dmx-icon-crop:before { content: "\e6c8"; } .dmx-icon-logo:before { content: "\e6c7"; } .dmx-icon-move:before { content: "\e6c6"; } .dmx-icon-folder:before { content: "\e6c4"; } .dmx-icon-folder-open:before { content: "\e6c5"; } .dmx-icon-delete:before { content: "\e6c2"; } .dmx-icon-edit:before { content: "\e6c3"; } .dmx-icon-export:before { content: "\e6c0"; } .dmx-icon-import:before { content: "\e6c1"; } .dmx-icon-history:before { content: "\e6bf"; } .dmx-icon-oxford:before { content: "\e6be"; } .dmx-icon-thefree:before { content: "\e6bd"; } .dmx-icon-wikipedia:before { content: "\e6b0"; } .dmx-icon-dreye:before { content: "\e6bc"; } .dmx-icon-eudic:before { content: "\e6bb"; } .dmx-icon-etymonline:before { content: "\e6b1"; } .dmx-icon-rrdict:before { content: "\e6b9"; } .dmx-icon-lexico:before { content: "\e6ba"; } .dmx-icon-urban:before { content: "\e6b8"; } .dmx-icon-macmillan:before { content: "\e6b7"; } .dmx-icon-vocabulary:before { content: "\e6b6"; } .dmx-icon-wordreference:before { content: "\e6b5"; } .dmx-icon-collins:before { content: "\e6b4"; } .dmx-icon-iciba:before { content: "\e6b3"; } .dmx-icon-hjdict:before { content: "\e6b2"; } .dmx-icon-dictionary:before { content: "\e6af"; } .dmx-icon-so:before { content: "\e6aa"; } .dmx-icon-merriam:before { content: "\e6ab"; } .dmx-icon-longman:before { content: "\e6ac"; } .dmx-icon-cambridge:before { content: "\e6ad"; } .dmx-icon-dictcn:before { content: "\e6ae"; } .dmx-icon-sound-us:before { content: "\e674"; } .dmx-icon-sound-zh:before { content: "\e675"; } .dmx-icon-sound-fr:before { content: "\e676"; } .dmx-icon-sound-de:before { content: "\e677"; } .dmx-icon-sound-th:before { content: "\e69c"; } .dmx-icon-sound-ru:before { content: "\e69d"; } .dmx-icon-sound-pt:before { content: "\e69e"; } .dmx-icon-sound-uk:before { content: "\e69f"; } .dmx-icon-sound-jp:before { content: "\e6a0"; } .dmx-icon-sound-el:before { content: "\e6a1"; } .dmx-icon-sound-cn:before { content: "\e6a2"; } .dmx-icon-sound-nl:before { content: "\e6a3"; } .dmx-icon-sound-it:before { content: "\e6a4"; } .dmx-icon-sound-sp:before { content: "\e6a5"; } .dmx-icon-sound-ko:before { content: "\e6a6"; } .dmx-icon-sound-ar:before { content: "\e6a7"; } .dmx-icon-sound-en:before { content: "\e6a8"; } .dmx-icon-sound-pl:before { content: "\e6a9"; } .dmx-icon-youdao:before { content: "\e693"; } .dmx-icon-sound-square:before { content: "\e69b"; } .dmx-icon-sound:before { content: "\e67a"; } .dmx-icon-user:before { content: "\e699"; } .dmx-icon-angle-double-right:before { content: "\e696"; } .dmx-icon-angle-double-left:before { content: "\e697"; } .dmx-icon-angle-double-up:before { content: "\e698"; } .dmx-icon-angle-double-down:before { content: "\e69a"; } .dmx-icon-youdaonote:before { content: "\e691"; } .dmx-icon-youtube:before { content: "\e692"; } .dmx-icon-windows:before { content: "\e694"; } .dmx-icon-yahoo:before { content: "\e695"; } .dmx-icon-voice-off:before { content: "\e68a"; } .dmx-icon-voice-on:before { content: "\e68b"; } .dmx-icon-voice:before { content: "\e68c"; } .dmx-icon-video:before { content: "\e68d"; } .dmx-icon-vip-crown:before { content: "\e68e"; } .dmx-icon-vip-fill:before { content: "\e68f"; } .dmx-icon-vip-card:before { content: "\e690"; } .dmx-icon-up:before { content: "\e688"; } .dmx-icon-up-circle:before { content: "\e689"; } .dmx-icon-trash:before { content: "\e682"; } .dmx-icon-twitter:before { content: "\e683"; } .dmx-icon-tool:before { content: "\e684"; } .dmx-icon-telegram:before { content: "\e685"; } .dmx-icon-tinder:before { content: "\e686"; } .dmx-icon-twitter-circle:before { content: "\e687"; } .dmx-icon-switch:before { content: "\e67f"; } .dmx-icon-switch-on:before { content: "\e680"; } .dmx-icon-switch-off:before { content: "\e681"; } .dmx-icon-success:before { content: "\e67d"; } .dmx-icon-suse:before { content: "\e67e"; } .dmx-icon-star:before { content: "\e67b"; } .dmx-icon-stop:before { content: "\e67c"; } .dmx-icon-sound-big:before { content: "\e673"; } .dmx-icon-sound-line:before { content: "\e678"; } .dmx-icon-sound-horn:before { content: "\e679"; } .dmx-icon-sogou:before { content: "\e66d"; } .dmx-icon-smile:before { content: "\e66e"; } .dmx-icon-skull:before { content: "\e66f"; } .dmx-icon-search:before { content: "\e670"; } .dmx-icon-s:before { content: "\e671"; } .dmx-icon-setting:before { content: "\e672"; } .dmx-icon-reply:before { content: "\e668"; } .dmx-icon-redhat:before { content: "\e669"; } .dmx-icon-right:before { content: "\e66a"; } .dmx-icon-reload:before { content: "\e66b"; } .dmx-icon-refresh:before { content: "\e66c"; } .dmx-icon-plus:before { content: "\e665"; } .dmx-icon-play:before { content: "\e666"; } .dmx-icon-qq:before { content: "\e667"; } .dmx-icon-plane-send:before { content: "\e65c"; } .dmx-icon-plane-departure:before { content: "\e65d"; } .dmx-icon-plane-up:before { content: "\e65e"; } .dmx-icon-plane-paper:before { content: "\e65f"; } .dmx-icon-plane:before { content: "\e660"; } .dmx-icon-plane-fly:before { content: "\e661"; } .dmx-icon-plane-up-fill:before { content: "\e662"; } .dmx-icon-plane-send-fill:before { content: "\e663"; } .dmx-icon-plane-takeoff:before { content: "\e664"; } .dmx-icon-picture-fill:before { content: "\e658"; } .dmx-icon-pin:before { content: "\e659"; } .dmx-icon-pin-left:before { content: "\e65a"; } .dmx-icon-picture:before { content: "\e65b"; } .dmx-icon-microsoft:before { content: "\e651"; } .dmx-icon-m:before { content: "\e652"; } .dmx-icon-modx:before { content: "\e653"; } .dmx-icon-meetup:before { content: "\e654"; } .dmx-icon-mule:before { content: "\e655"; } .dmx-icon-music:before { content: "\e656"; } .dmx-icon-more:before { content: "\e657"; } .dmx-icon-loading:before { content: "\e650"; } .dmx-icon-l:before { content: "\e64a"; } .dmx-icon-itunes:before { content: "\e64b"; } .dmx-icon-info:before { content: "\e64c"; } .dmx-icon-laugh:before { content: "\e64d"; } .dmx-icon-left:before { content: "\e64e"; } .dmx-icon-list:before { content: "\e64f"; } .dmx-icon-help:before { content: "\e647"; } .dmx-icon-horn:before { content: "\e648"; } .dmx-icon-home:before { content: "\e649"; } .dmx-icon-headset:before { content: "\e644"; } .dmx-icon-headset-c:before { content: "\e645"; } .dmx-icon-heart:before { content: "\e646"; } .dmx-icon-good:before { content: "\e642"; } .dmx-icon-google:before { content: "\e643"; } .dmx-icon-fullscreen-exit:before { content: "\e63e"; } .dmx-icon-fullscreen:before { content: "\e63f"; } .dmx-icon-fan:before { content: "\e640"; } .dmx-icon-flag:before { content: "\e641"; } .dmx-icon-eye-left:before { content: "\e639"; } .dmx-icon-error:before { content: "\e63a"; } .dmx-icon-eye-right:before { content: "\e63b"; } .dmx-icon-exchange:before { content: "\e63c"; } .dmx-icon-edge:before { content: "\e63d"; } .dmx-icon-down:before { content: "\e634"; } .dmx-icon-diaspora:before { content: "\e635"; } .dmx-icon-devil:before { content: "\e636"; } .dmx-icon-degree:before { content: "\e637"; } .dmx-icon-d:before { content: "\e638"; } .dmx-icon-close:before { content: "\e62e"; } .dmx-icon-circle:before { content: "\e62f"; } .dmx-icon-cry-fill:before { content: "\e630"; } .dmx-icon-copy:before { content: "\e631"; } .dmx-icon-cry:before { content: "\e632"; } .dmx-icon-close-bg:before { content: "\e633"; } .dmx-icon-bing:before { content: "\e62b"; } .dmx-icon-bell:before { content: "\e62c"; } .dmx-icon-baidu:before { content: "\e62d"; } .dmx-icon-arrow-right:before { content: "\e625"; } .dmx-icon-arrow-left:before { content: "\e626"; } .dmx-icon-arrow-up:before { content: "\e627"; } .dmx-icon-arrow-down:before { content: "\e628"; } .dmx-icon-arrow-thin-down:before { content: "\e629"; } .dmx-icon-arrow-thin-up:before { content: "\e62a"; } .dmx-icon-android:before { content: "\e622"; } .dmx-icon-apple:before { content: "\e623"; } .dmx-icon-angular:before { content: "\e624"; } .dmx-icon-alibaba:before { content: "\e621"; } ================================================ FILE: src/css/dmx_dialog.css ================================================ p, h1, h2, h3, h4, h5, h6, ul, li, ol, dl, dt, dd { margin: 0; padding: 0; list-style: none; } h1, h2, h3, h4, h5, h6 { font-size: 18px; } blockquote { margin: 10px 0; padding-left: 5px; border-left: 6px solid #e4dfca; } .dmx_unselectable { -webkit-user-select: none !important; -moz-user-select: none !important; -ms-user-select: none !important; user-select: none !important; } .dmx_overflow_hidden { overflow: hidden; } .dmx_hide { display: none; } .dmx_show { display: block; } #dmx_mouse_icon, #dmx_crop_bg, #dmx_crop_fg { display: none; position: fixed; top: 0; left: 0; } #dmx_mouse_icon { z-index: 999999999999; font-family: "dmx-icon", serif !important; width: 32px; height: 32px; font-size: 18px; font-style: normal; background: #4395ff; border: 1px solid #3589ff; box-shadow: 0 1px 3px rgba(0, 0, 0, .19), inset 0 1px 0 rgba(255, 255, 255, .4); border-radius: 50%; color: #fff; box-sizing: border-box; justify-content: center; align-items: center; cursor: pointer; transition: transform 0.25s ease; } #dmx_mouse_icon:before { content: "\e661"; } #dmx_mouse_icon:hover { background: linear-gradient(#FF416C, #FF4B2B); border: 1px solid #FF416C; } #dmx_mouse_icon:hover:before { transform: rotate(-30deg); } #dmx_crop_bg { z-index: 9999999999998; width: 100%; height: 100%; background: #000; opacity: .2; cursor: crosshair; } #dmx_crop_fg { z-index: 9999999999999; width: 1px; height: 1px; background: #fff; opacity: .7; border: 1px dashed #000; } #dmx_dialog { font-family: Roboto, 'Segoe UI', Arial, 'Microsoft Yahei', sans-serif; /*font-family: Helvetica Neue, PingFang SC, Microsoft YaHei, sans-serif;*/ all: initial; position: fixed; z-index: 999999999998; width: 500px; height: 500px; /*overflow: hidden;*/ background: #f5f8fa; border: 1px solid #cfd0d2; border-radius: 6px; /*top: calc(50% - 250px);*/ /*left: calc(50% - 250px);*/ right: 0; bottom: 0; box-sizing: border-box; box-shadow: 0 0 12px -2px rgba(32, 32, 32, .5); display: none; } #dmx_dialog.dmx_keep_right { height: auto; top: 0; right: 0; bottom: 0; } #dmx_dialog.dmx_popup { top: 0; left: 0; width: 520px; height: 500px; border: 0; border-radius: 0; } #dmx_dialog.dmx_popup.fullscreen { width: 100%; height: 100%; min-width: 450px; min-height: 300px; } #dmx_dialog_resize_n { cursor: ns-resize; position: absolute; z-index: 999999999999; left: 0; top: -5px; width: 100%; height: 8px } #dmx_dialog_resize_e { cursor: ew-resize; position: absolute; z-index: 999999999999; right: -7px; top: 0; width: 8px; height: 100% } #dmx_dialog_resize_s { cursor: ns-resize; position: absolute; z-index: 999999999999; left: 0; bottom: -5px; width: 100%; height: 8px } #dmx_dialog_resize_w { cursor: ew-resize; position: absolute; z-index: 999999999999; left: -5px; top: 0; width: 8px; height: 100% } #dmx_dialog_resize_nw { cursor: nwse-resize; position: absolute; z-index: 999999999999; left: -5px; top: -5px; width: 17px; height: 17px; } #dmx_dialog_resize_ne { cursor: nesw-resize; position: absolute; z-index: 999999999999; right: -5px; top: -5px; width: 10px; height: 10px } #dmx_dialog_resize_sw { cursor: nesw-resize; position: absolute; z-index: 999999999999; left: -5px; bottom: -5px; width: 10px; height: 10px } #dmx_dialog_resize_se { cursor: nwse-resize; position: absolute; z-index: 999999999999; right: -5px; bottom: -5px; width: 17px; height: 17px; } #dmx_dialog_title { height: 32px; line-height: 32px; padding: 0 10px; background: #f5f8fa; border-bottom: 1px solid #cfd0d2; border-radius: 6px 6px 0 0; cursor: move; } #dmx_dialog_title .dmx-icon { cursor: pointer; color: #232B33; } #dmx_dialog_title .dmx-icon:hover { opacity: 0.7; } #dmx_dialog_title .dmx_left { float: left; } #dmx_dialog_title .dmx_left > div { float: left; margin-right: 8px; } #dmx_dialog_title #dmx_close .dmx-icon { color: #FC4A4A; font-size: 18px; } #dmx_dialog_title #dmx_pin:before { content: "\e65a"; padding: 2px; font-size: 18px; } #dmx_dialog_title #dmx_pin.active:before { content: "\e659"; background: #e0e3e6; border-radius: 2px; } #dmx_dialog_title #dmx_fullscreen:before { content: "\e63f"; } #dmx_dialog_title #dmx_fullscreen.active:before { content: "\e63e"; } #dmx_dialog_title .dmx-icon-voice { font-size: 18px; } #dmx_dialog_title .dmx-icon-heart { color: #FC4A4A; } #dmx_dialog_title #dmx_history { margin: 2px 0 0 15px; border: 1px solid gray; border-radius: 30px; height: 26px; overflow: hidden; } #dmx_dialog_title #dmx_history .dmx-icon { display: block; float: left; margin: 0; line-height: 26px; } #dmx_dialog_title #dmx_history .dmx-icon:hover { background: #252525; color: #fff; } #dmx_dialog_title #dmx_history .dmx-icon-left { padding: 0 8px; border-right: 1px solid gray; } #dmx_dialog_title #dmx_history .dmx-icon-right { padding: 0 8px; } #dmx_dialog_title #dmx_history .dmx-icon.disabled { color: #9EA0A3; background: #F3F6F9; opacity: 0.8; } #dmx_dialog.dmx_popup #dmx_history { margin-left: 0; } #dmx_dialog_title #dmx_navigate { margin: 1px 0 0 15px; } #dmx_dialog_title #dmx_navigate u { float: left; display: block; background: #F3F6F9; border: 1px solid #C4C5C8; border-radius: 5px 5px 0 0; padding: 0 8px; margin-right: 5px; height: 30px; line-height: 30px; overflow: hidden; font-size: 15px; font-weight: 500; text-decoration: none; cursor: pointer; } #dmx_dialog_title #dmx_navigate u.active { border-bottom: 1px solid #F3F6F9; opacity: 1; } #dmx_dialog_title .dmx_right { float: right; } #dmx_dialog_title .dmx_right > div { float: right; margin-left: 8px; } #dmx_dialog_content { box-sizing: border-box; position: relative; margin-top: 2px; width: 100%; height: calc(100% - 35px); overflow: auto; } #dmx_iframe { box-sizing: border-box; width: 100%; height: calc(100% - 6px); border: none; } #dmx_head { /*position: sticky;*/ box-sizing: border-box; width: 100%; height: 45px; padding: 2px 10px 5px; background: #F3F6F9; border-bottom: 1px solid rgba(32, 33, 36, 0.28); box-shadow: 0 1px 4px 0 rgba(32, 33, 36, 0.18); } .dmx_content { box-sizing: border-box; position: absolute; left: 0; top: 45px; width: 100%; height: calc(100% - 45px); overflow: auto; } .dmx_main { margin: 0 auto; padding: 10px; min-width: 100px; font-size: 15px; line-height: 1.8; } .dmx_main_trans { padding-bottom: 0; } .dmx_main_search { padding: 10px 0 0 10px; } #dmx_dialog_left { position: absolute; bottom: 0; left: -37px; width: 37px; } #dmx_dialog_left a { display: block; width: 32px; height: 32px; line-height: 30px; font-size: 16px; background: #4395ff; border: 1px solid #3589ff; box-shadow: 0 1px 3px rgba(0, 0, 0, .19), inset 0 1px 0 rgba(255, 255, 255, .4); border-radius: 50%; color: #fff; box-sizing: border-box; text-align: center; margin-top: 5px; } .dmx_loading { border: 3px solid #bdc3c7; border-top: 3px solid #2c3e50; border-bottom: 3px solid #2c3e50; border-radius: 50%; display: block; width: 16px; height: 16px; animation: spin 1.2s linear infinite; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } ::-webkit-scrollbar { width: 5px; height: 5px; } ::-webkit-scrollbar-track { /*background-color: #E2E4E6;*/ } ::-webkit-scrollbar-thumb { background-color: #bdc3c7; border-radius: 5px; } ::-webkit-scrollbar-thumb:hover { background-color: #9da5ac; } ::-webkit-scrollbar-thumb:active { background-color: #2c3e50; } p, h1, h2, h3, h4, h5, h6 { margin: 0; padding: 0; } a { text-decoration: none; color: #333; } a:hover { color: #1a73e8; } .fx:after { display: block; content: ''; clear: both; } .mt-1 { margin-top: 10px; } .mb-1 { margin-bottom: 10px; } /* ==================== translate ==================== */ #translate_input { background: #fff; color: #4e5053; min-height: 26px; line-height: 1.6; padding: 5px 10px; outline: none; overflow: auto; } #translate_input:focus { border-color: #409eff; } #translate_input.big { min-height: 52px; } .language_box { position: relative; } .language_box > div { float: left; } .language_button { background: #ffffff; border: 1px solid #cfd0d2; border-radius: 8px; margin-top: 10px; padding: 2px 10px; min-width: 80px; height: 28px; line-height: 28px; text-align: center; user-select: none; cursor: pointer; } .language_button:hover { color: #1a73e8; box-shadow: -2px -2px 2px 0 #d0d7de; } .language_button:after { font-size: 8px; margin-left: 6px; content: "\e629"; } .language_button.active { z-index: 999; position: relative; border-radius: 8px 8px 0 0; border-bottom: 1px solid #fff; box-shadow: -2px 0 0 0 #d0d7de; height: 31px; } .language_button.active:after { content: "\e62a"; } #language_dropdown { z-index: 998; position: absolute; left: 0; top: 46px; box-sizing: border-box; width: 100%; background: #fff; border: 1px solid #cfd0d2; border-radius: 0 8px 8px 8px; padding: 10px 10px 5px 10px; margin-bottom: 3px; box-shadow: 0 2px 2px 2px #d0d7de; display: none; } #language_dropdown.dropdown_target { border-radius: 8px; } #language_dropdown.dropdown_target u:first-child { display: none; } #language_dropdown u { float: left; display: block; text-decoration: none; background: #f5f5f5; border: 1px solid #d3d3d3; border-radius: 5px; color: rgba(0, 0, 0, 0.87); padding: 2px 5px; margin: 0 5px 5px 0; cursor: pointer; } #language_dropdown u:hover { background: #414345; border: 1px solid #232526; color: #fff; } #language_dropdown u.active { background: #e8f0fe; border: 1px solid #185abc; color: #185abc; } #language_dropdown u.disabled { background: #f5f5f5; border: 1px solid #d3d3d3; color: #ddd; cursor: not-allowed; } #language_exchange { margin-top: 10px; padding: 2px 8px; cursor: pointer; } #language_exchange.disabled { color: #ddd; cursor: not-allowed; } #translate_crop { float: right; background: #fff; border: 1px solid #dcdfe6; color: #606266; border-radius: 8px; height: 28px; line-height: 28px; padding: 2px 7px; margin: 10px 10px 0 0; cursor: pointer; } #translate_button { float: right; background: #4395ff; border: 1px solid #3589ff; border-radius: 8px; color: #fff; height: 28px; line-height: 28px; margin-top: 10px; padding: 2px 15px; cursor: pointer; user-select: none; transition: .1s; } #translate_button:hover, #translate_crop:hover { box-shadow: -2px -2px 2px 0 #d0d7de; } #translate_button:active { opacity: .7; } .case { font-size: 16px; margin-top: 10px; border-radius: 8px; background: #ffffff; border: 1px solid #cfd0d2; transition: all .2s linear; } .case:hover { box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.37); /*transform: scale(1.005);*/ } .case:first-child { margin-top: 0; } .case .dmx-icon { cursor: pointer; } .case .dmx-icon:hover { color: #1a73e8; } .case_copy, .case_bilingual { display: none; } .case:hover .case_copy, .case:hover .case_bilingual { display: block; } .case_content { padding: 7px 10px; line-height: 1.6; } .case_content > div, .case_dd > div { margin-top: 5px; } .case_content > div:first-child, .case_dd > div:first-child { padding-top: 0 !important; } .case_content [data-search="true"] { cursor: pointer; } .case_content p.source_text { color: #7398e6; margin-top: 5px; } .case_content p.source_text:first-child { margin-top: 0; } .case_dd { margin-top: 10px; border-top: thin dashed #ccc; } .case_dd_head { padding: 10px 0 0; font-weight: bold; font-size: 110%; } .case_dd_ph .dmx-icon { margin-left: 5px; } .case_dd_parts p { padding: 3px 0 0; } .case_dd_parts p:first-child { padding: 0; } .case_dd_parts p b { color: #409eff; background: #ecf5ff; border: 1px solid #d9ecff; border-radius: 4px; padding: 0 5px; margin-right: 10px; } .case_dd_exchange b { font-weight: 500; } .case_dd_exchange u { text-decoration: none; margin-right: 10px; } .case_dd_exchange u a { margin-left: 5px; } .case_dd_chart { color: #e6a23c; background-color: #fdf6ec; border: 1px solid #faecd8; border-radius: 5px; padding: 5px; } .case_dd_chart u { text-decoration: none; margin: 0 10px 0 5px; color: #ffc942; } .case_dd_example em { font-style: normal; color: #e6a23c; } .case_dd_tags u { text-decoration: none; display: inline-block; margin-right: 5px; padding: 0 5px; color: #909399; background: #f4f4f5; border: 1px solid #e9e9eb; border-radius: 4px; font-size: 90%; } .case_dd_img a { margin-right: 5px; } .case_dd_img img { border: 1px solid #ccc; width: 80px; height: 80px; object-fit: contain; } .case_top { padding: 2px 10px; border-bottom: 1px solid rgba(0, 0, 0, 0.1); } .case_bottom { padding: 2px 10px; border-top: 1px solid rgba(0, 0, 0, 0.1); } .case_left { float: left; margin-right: 10px; } .case_left:last-child { margin-right: 0; } .case_right { float: right; margin-left: 15px; } .case_right:last-child { margin-left: 0; } .case_right a { font-weight: bold; } .case_right a .dmx-icon { margin-right: 3px; } .case_bilingual { cursor: pointer; } .case_bilingual:after { font-size: 22px; line-height: 1; display: inline-block; vertical-align: middle; margin-left: 3px; content: "\e681"; color: rgba(0, 0, 0, 0.26); } .case_bilingual.active:after { content: "\e680"; color: #1a73e8; } .dmx_ripple { position: relative; display: inline-block; vertical-align: middle; text-align: center; width: 26px; height: 26px; line-height: 26px; font-size: 20px; color: #535353; cursor: pointer; transition: all .2s; } .dmx_ripple:hover { color: #1a73e8; transform: scale(1.3); } .dmx_ripple.active { color: #e53935; } .dmx_ripple.active:before, .dmx_ripple.active:after { display: block; position: absolute; content: ''; top: 0; right: 0; bottom: 0; left: 0; border-radius: 50%; border: 1px solid #e35d5b; } .dmx_ripple.active:before { animation: dmx_ripple 1s linear infinite; } .dmx_ripple.active:after { animation: dmx_ripple 1s linear 0.5s infinite; } .dmx_reverse:before, .dmx_reverse:after { animation-direction: reverse !important; } @keyframes dmx_ripple { 0% { transform: scale(1); } 50% { transform: scale(1.75); opacity: 0.5; } 75% { transform: scale(1.95); opacity: 0.2; } 100% { transform: scale(2.05); opacity: 0; } } .dmx_pink { color: deeppink; } /* ==================== search ==================== */ .search_box { background: #fff; color: #4e5053; height: 30px; line-height: 30px; padding: 3px 60px 3px 8px; position: relative; } .search_box:hover { border-color: #409eff; } .search_box > input { display: block; width: 100%; height: 28px; font-size: 17px; outline: none; border: none; } .search_box #search_but { position: absolute; top: calc(50% - 15px); right: 10px; } .search_box #search_remove { position: absolute; top: calc(50% - 15px); right: 35px; display: none; } .search_box:hover #search_remove { display: block; } /* ==================== alert ==================== */ #dmx_alert { z-index: 999999999999; position: fixed; top: 0; left: 50%; transform: translateX(-50%); width: 300px; } #dmx_alert > div { position: relative; padding: 10px; font-size: 15px; line-height: 1.5; box-sizing: border-box; color: #ffffff; background-color: #414345; border: 1px solid #232526; border-radius: 4px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, .1); transition: all .3s; overflow: hidden; } #dmx_alert > div.an_top { margin-top: 10px; } #dmx_alert > div.an_delete { opacity: 0.1; } #dmx_alert > div .dmx-icon { margin-right: 5px; } #dmx_alert .dxm_alert_success { background-color: #6FBD39; border: 1px solid #56ab2f; } #dmx_alert .dxm_alert_error { background-color: #ef473a; border: 1px solid #b31217; } /* ==================== button ==================== */ .dmx_button { display: inline-block; background: #4395ff; border: 1px solid #3589ff; border-radius: 6px; color: #fff; line-height: 1; margin: 0 10px 10px 0; padding: 8px 12px; white-space: nowrap; text-align: center; cursor: pointer; user-select: none; transition: .1s; } .dmx_button:last-child { margin: 0; } .dmx_button:hover { box-shadow: 1px 4px 6px 0 #d0d7de; } .dmx_button:active { opacity: .7; } .dmx_button .dmx-icon { margin-right: 5px; } .dmx_button_default { background: #fff; border: 1px solid #dcdfe6; color: #606266; } .dmx_button_success { background-color: #67c23a; border-color: #67c23a; color: #fff; } .dmx_button_warning { background-color: #e6a23c; border-color: #e6a23c; color: #fff; } .dmx_button_danger { background-color: #ef473a; border-color: #dd1419; color: #fff; } /* dict */ .dict_cambridge .sense-body { margin-top: 10px; } .dict_cambridge .dxref { margin-right: 3px; padding: 2px 6px; color: #fff; font-weight: 700; font-size: .75rem; text-align: center; background: #1d2956; border-radius: 50px; } .dict_cambridge .ddef_h { margin-bottom: 10px; font-weight: 700; } .dict_cambridge .dsense_h, .dict_cambridge .ca_h { margin-top: 10px; font-weight: 700; color: #5d2fc1; } .dict_cambridge .dtrans { display: block; color: #0580e8; } .dict_collins .dictlink { margin-top: 10px; } .dict_collins .dictlink:first-child { margin-top: 0; } .dict_collins .sensenum, .dict_collins .def, .dict_collins .word-frequency-container .label { display: inline-block; } .dict_collins .title_frequency_container { color: #c12d30; } .dict_collins .pos { text-transform: uppercase; font-weight: bold; color: #c12d30; } .dict_collins .hom { margin-top: 10px; } .dict_collins .hom:first-of-type { margin-top: 0; } .dict_collins .imageRight { float: right; width: 30%; min-width: 150px; padding: 2px; border: 1px solid #bbb; border-radius: 5px; margin: 0 0 5px 5px; } .dict_etymonline [class^="word__name--"] { font-size: 120%; font-weight: 700; color: #0580e8; } .dict_oxford .examples { margin-left: 10px; } .dict_wr .rh_pdef { display: block; } .dict_macmillan .dflex { display: flex; flex: 1 1 auto; flex-wrap: nowrap; } .dict_macmillan .SENSE-NUM, .dict_macmillan .note { display: inline-block; min-width: 1.2em; height: 1.2em; line-height: 1.2em; padding: .2em; margin-right: .2em; border-radius: 1em; background: #d70a20; text-align: center; font-weight: bold; color: white; } ================================================ FILE: src/css/longman.css ================================================ .longman_dict .Sense img { float: right; width: 30%; max-width: 400px; margin-left: 5px; } .longman_dict .Sense:after { display: block; content: ''; clear: both; } .longman_dict .EXAMPLE .dmx-icon { margin-right: 5px; } .longman_dict .infls, .longman_dict .description, .longman_dict .title, .longman_dict .url, .longman_dict .summary, .longman_dict .og, .longman_dict .infls, .longman_dict .description, .longman_dict .title, .longman_dict .url, .longman_dict .summary, .longman_dict .og, .longman_dict .assetref, .longman_dict .assetref, .longman_dict .dictentry, .longman_dict .dictionary_intro, .longman_dict .topic_intro, .longman_dict .exaGroup, .longman_dict .exaGroup .exaEntry, .longman_dict .exaGroup .title, .longman_dict .exaGroup .exa, .ldoceEntry .Entry, .ldoceEntry .ErrorBox, .ldoceEntry .EXAMPLE, .ldoceEntry .GramExa, .ldoceEntry .PhrVbEntry, .ldoceEntry .RunOn, .ldoceEntry .Sense, .ldoceEntry .Subsense, .ldoceEntry .ColloBox, .ldoceEntry .ThesBox, .ldoceEntry .F2NBox, .ldoceEntry .GramBox, .ldoceEntry .HEADING.newline, .ldoceEntry .Collocate, .ldoceEntry .Exponent, .ldoceEntry .EXPL, .ldoceEntry .COLLEXA, .ldoceEntry .THESEXA, .ldoceEntry .LearnerItem, .ldoceEntry .ColloExa, .bussdictEntry .Box, .bussdictEntry .Entry, .bussdictEntry .ColloExa, .bussdictEntry .ErrorBox, .bussdictEntry .EXAMPLE, .bussdictEntry .GramExa, .bussdictEntry .PhrVbEntry, .bussdictEntry .Sense, .bussdictEntry .Subsense, .bussdictEntry .ColloBox, .bussdictEntry .ThesBox, .bussdictEntry .F2NBox, .bussdictEntry .GramBox, .bussdictEntry .UsageBox, .bussdictEntry .HEADING.newline, .bussdictEntry .SECHEADING, .bussdictEntry .subheading, .bussdictEntry .Collocate.newline, .bussdictEntry .Exponent, .bussdictEntry .GramBox .EXPL, .bussdictEntry .EXPL.newline, .bussdictEntry .LearnerItem, .bussdictEntry .CompareWord, .bussdictEntry .EXP, .bussdictEntry .boxheader, .bussdictEntry .SubEntry { display: block; } .ldoceEntry .FIELD, .ldoceEntry .HWD, .ldoceEntry .NOTE, .ldoceEntry .Noteprompt, .ldoceEntry .PIC, .ldoceEntry .PICCAL, .ldoceEntry .ACTIV, .ldoceEntry .BOX, .ldoceEntry .COMMENT, .ldoceEntry .USAGE, .bussdictEntry .ACTIV, .bussdictEntry .COMMENT, .bussdictEntry .FIELD, .bussdictEntry .HWD, .bussdictEntry .NOTE, .bussdictEntry .Noteprompt, .bussdictEntry .PIC, .bussdictEntry .PICCAL, .bussdictEntry .FIELDXX, .bussdictEntry .USAGE, .bussdictEntry .Crossrefto .REFLEX, .longman_dict .suppressed, .longman_dict .verbTable .view_less, .longman_dict .verbTable .next_tenses { display: none; } .bussdictEntry .SubEntry .HWD, .bussdictEntry .GramBox.nobox, .bussdictEntry .GramBox.nobox .EXPL, .bussdictEntry .Collocate.inline { display: inline; } .longman_dict .Crossrefto, .longman_dict .assettype, .longman_dict .exaGroup .title, .longman_dict .exaGroup .NodeW, .longman_dict .wordfams, .longman_dict .etym .Head, .longman_dict .verbTable .lemma, .longman_dict .verbTable .Simple_Form .aux, .longman_dict .verbTable .col1, .longman_dict .verbTable .view_more, .longman_dict .verbTable .view_less, .longman_dict .verbTable .verb_form, .ldoceEntry .ABBR, .ldoceEntry .AMEQUIV, .ldoceEntry .BREQUIV, .ldoceEntry .COLLO, .ldoceEntry .COLLOINEXA, .ldoceEntry .COMP, .ldoceEntry .Crossrefto, .ldoceEntry .DERIV, .ldoceEntry .FULLFORM, .ldoceEntry .GRAM, .ldoceEntry .HINTBOLD, .ldoceEntry .HINTTITLE, .ldoceEntry .HOMNUM, .ldoceEntry .HYPHENATION, .ldoceEntry .PHRVBHWD, .ldoceEntry .LEXUNIT, .ldoceEntry .LEXVAR, .ldoceEntry .OPP, .ldoceEntry .ORTHVAR, .ldoceEntry .PASTPART, .ldoceEntry .PASTTENSE, .ldoceEntry .PLURALFORM, .ldoceEntry .POS, .ldoceEntry .PRESPART, .ldoceEntry .PRESPARTX, .ldoceEntry .PROPFORM, .ldoceEntry .PROPFORMPREP, .ldoceEntry .PTandPP, .ldoceEntry .PTandPPX, .ldoceEntry .REFHWD, .ldoceEntry .REFLEX, .ldoceEntry .RELATEDWD, .ldoceEntry .SIGNPOST, .ldoceEntry .SUPERL, .ldoceEntry .SYN, .ldoceEntry .T3PERSSING, .ldoceEntry .T3PERSSINGX, .ldoceEntry .UNCLASSIFIED, .ldoceEntry .warning, .ldoceEntry .sensenum, .ldoceEntry .synopp, .ldoceEntry .FREQ, .ldoceEntry .AC, .ldoceEntry .HEADING, .ldoceEntry .heading, .ldoceEntry .SECHEADING, .ldoceEntry .subheading, .ldoceEntry .COLLOC, .ldoceEntry .EXP, .ldoceEntry .EXPR, .ldoceEntry .keycollo, .ldoceEntry .THESPROPFORM, .ldoceEntry .DEFBOLD, .bussdictEntry .GRAM, .bussdictEntry .POS, .bussdictEntry .ABBR, .bussdictEntry .AMEQUIV, .bussdictEntry .BREQUIV, .bussdictEntry .COLLO, .bussdictEntry .COMP, .bussdictEntry .COLLOINEXA, .bussdictEntry .DERIV, .bussdictEntry .FREQ, .bussdictEntry .LEVEL, .bussdictEntry .FULLFORM, .bussdictEntry .HINTBOLD, .bussdictEntry .HINTTITLE, .bussdictEntry .HOMNUM, .bussdictEntry .HYPHENATION, .bussdictEntry .LEXUNIT, .bussdictEntry .LEXVAR, .bussdictEntry .OPP, .bussdictEntry .ORTHVAR, .bussdictEntry .PASTPART, .bussdictEntry .PASTTENSE, .bussdictEntry .PHRVBHWD, .bussdictEntry .PLURALFORM, .bussdictEntry .PRESPART, .bussdictEntry .PRESPARTX, .bussdictEntry .PROPFORM, .bussdictEntry .PROPFORMPREP, .bussdictEntry .PTandPP, .bussdictEntry .PTandPPX, .bussdictEntry .REFLEX, .bussdictEntry .RELATEDWD, .bussdictEntry .SIGNPOST, .bussdictEntry .SUPERL, .bussdictEntry .SYN, .bussdictEntry .T3PERSSING, .bussdictEntry .T3PERSSINGX, .bussdictEntry .UNCLASSIFIED, .bussdictEntry .THESPROPFORM, .bussdictEntry .SECHEADING, .bussdictEntry .subheading, .bussdictEntry .HEADING, .bussdictEntry .heading, .bussdictEntry .warning, .bussdictEntry .sensenum, .bussdictEntry .synopp, .bussdictEntry .Gramref, .bussdictEntry .COLLOC, .bussdictEntry .EXP, .bussdictEntry .EXPR, .bussdictEntry .keycollo, .bussdictEntry .DEFBOLD, .bussdictEntry .boxheader, .bussdictEntry .SubEntry .HWD { font-weight: bold; } .ldoceEntry .REFHOMNUM, .bussdictEntry .REFHOMNUM { vertical-align: super; font-size: 60%; } .bussdictEntry .HOMNUM, .longman_dict .etym .Head .HOMNUM { vertical-align: super; font-size: 12pt; } .ldoceEntry .AC, .ldoceEntry .synopp { color: #fff; border-color: #f1d600; background-color: #f1d600; } .ldoceEntry .EXPL .cross, .ldoceEntry .GramBox .EXPL .dont_say, .ldoceEntry .BADEXA { color: red; } .ldoceEntry .FREQ, .bussdictEntry .FREQ, .bussdictEntry .LEVEL { color: red; border-color: red; } .ldoceEntry .LEVEL { color: red; font-size: 120%; } .ldoceEntry .frequent .HOMNUM, .ldoceEntry .HOMNUM { color: red; vertical-align: super; font-size: 12pt; } .ldoceEntry .frequent .HYPHENATION, .ldoceEntry .HYPHENATION, .ldoceEntry .PHRVBHWD { color: red; font-size: 160%; } .ldoceEntry .warning, .bussdictEntry .warning { color: red; font-style: normal; } .ldoceEntry .GramBox .CROSS .neutral { color: red; margin-right: 10px; } .Entry .topic, .topicCloud .topic_other { color: red; text-decoration: underline; } .ldoceEntry .GRAM, .bussdictEntry .GRAM { color: green; margin: 0 5px 10px 3px; } .ldoceEntry .POS, .bussdictEntry .POS { color: green; margin: 0 0 0 10px; } .longman_dict .pos { color: green; font-weight: normal; } .ldoceEntry .REGISTERLAB { color: purple; font-style: italic; } .ldoceEntry .SIGNPOST { background-color: #f18500; color: white; font-size: 79%; text-transform: uppercase; border-radius: 5px; padding: 0 3px; letter-spacing: 1px; } .longman_dict .Crossrefto, .ldoceEntry .RELATEDWD { color: blue; } .bussdictEntry .synopp { font-style: normal; color: darkblue; } .longman_dict .assettype, .ldoceEntry .Thesref, .ldoceEntry .GEO, .ldoceEntry .geo, .ldoceEntry .GLOSS, .ldoceEntry .LINKWORD, .ldoceEntry .keycollo, .bussdictEntry .LINKWORD, .bussdictEntry .COLLOC.key, .bussdictEntry .keycollo { color: #364395; } .bussdictEntry .Box, .ldoceEntry .ColloBox, .ldoceEntry .ThesBox, .ldoceEntry .F2NBox, .ldoceEntry .GramBox { border-radius: 5px; border: 1px solid #364395; padding: 10px; margin: 8px 0; } .ldoceEntry .HEADING, .ldoceEntry .heading, .bussdictEntry .SubEntry .HWD { color: #364395; font-size: 120%; } .bussdictEntry .GEO, .bussdictEntry .geo, .bussdictEntry .REGISTERLAB { color: #364395; font-style: italic; } .bussdictEntry .GLOSS { color: #364395; font-weight: normal; font-style: normal; } .bussdictEntry .PHRVBHWD, .bussdictEntry .HEADING, .bussdictEntry .heading, .bussdictEntry .Gramref { color: #364395; font-size: 120%; } .bussdictEntry .boxheader { background-color: #364395; color: #fff; } .longman_dict .dictionary_intro, .longman_dict .topic_intro { background-color: #35a3ff; color: #fff; padding-left: 10px; margin: 5px 0 10px -7px; } .longman_dict .assets_intro, .longman_dict .asset_intro { border: 1px solid #f1d600; background-color: #f1d600; color: #fff; font-weight: normal; border-radius: 5px; padding-left: 3px; padding-right: 3px; } .longman_dict .verbTable table { background-color: #fae660; margin-bottom: 10px; border-collapse: collapse; width: 100%; } .ldoceEntry .SECHEADING, .ldoceEntry .subheading { display: table; border-radius: 5px; border: solid #6f469d 2px; padding-left: 4px; padding-right: 15px; margin: 10px 0; color: white; text-transform: uppercase; background-color: #6f469d; } .ldoceEntry .amefile { color: #4693db; font-size: 130%; padding-left: 5px; } .ldoceEntry .brefile { color: #fa6360; font-size: 130%; padding-left: 5px; } .ldoceEntry .neutral, .verbTable .aux, .verbTable .verb_form, .ldoceEntry .sensenum { color: black; } .ldoceEntry .sensenum { font-style: normal; } .longman_dict .GramBox { color: black; background-color: #fff; } .bussdictEntry .Box .EXAMPLE { color: black; display: inline; margin-left: 0; font-style: italic; } .verbTable .lemma { color: black; font-size: 120%; margin: 0 0 0 10px; } .verbTable .view_more, .verbTable .view_less { color: black; text-align: center; padding: 20px 0 10px; } .verbTable .geo { color: black; font-style: italic; font-weight: normal; } .longman_dict .exaGroup .title { color: black; font-size: 110%; margin: 10px 0 0 5px; } .longman_dict .Entry .related_topics { color: black; padding: 0 4px 1px 0; font-size: 16px; } .ldoceEntry .COLLEXA, .ldoceEntry .THESEXA { color: gray; } .ldoceEntry .exafile { color: gray; font-style: normal; font-size: 120%; padding: 5px; } .ldoceEntry .EXAMPLE, .bussdictEntry .EXAMPLE, .longman_dict .exaGroup .exa { color: gray; margin-left: 10px; } .bussdictEntry .supp, .bussdictEntry .SIGNPOST { background-color: gray; } .longman_dict .verbTable .col2 { color: gray; width: 150px } .longman_dict a { border-bottom: thin dotted gray; } .ldoceEntry .RunOn { margin-bottom: 10px; } .longman_dict .right_col .yellow_box { margin-bottom: 10px; display: inline-block; } .longman_dict .dictentry, .longman_dict .exaGroup .exaEntry, .longman_dict .exaGroup .exaGroup, .longman_dict .verbTable .entry { margin-bottom: 20px; } .ldoceEntry .Sense, .bussdictEntry .Sense, .longman_dict .verbTable .entry { margin-left: 20px; margin-bottom: 15px; } .bussdictEntry .SubEntry.embedded { margin-top: -5px; margin-left: 20px; margin-bottom: 0; } .bussdictEntry .SubEntry { margin-top: 2px; margin-bottom: 2px; } .longman_dict .yellow_box { margin-top: 20px; display: table; } .ldoceEntry .Entry, .bussdictEntry .Entry { font-size: 12pt; text-align: justify; margin-top: 8px; } .ldoceEntry .Subsense, .bussdictEntry .ColloExa, .bussdictEntry .GramExa, .bussdictEntry .Subsense { margin-left: 10px; } .ldoceEntry .PROPFORM, .ldoceEntry .PROPFORMPREP, .ldoceEntry .COLLO, .bussdictEntry .SubEntry { margin-left: 20px; } .ldoceEntry .Crossrefto, .ldoceEntry .DERIV { font-size: 120%; } .ldoceEntry .Entry { font-size: 11pt; text-align: justify; } .ldoceEntry .HINTITALIC, .ldoceEntry .STRONG, .ldoceEntry .GOODCOLLO, .bussdictEntry .COLLOINEXA, .bussdictEntry .HINTITALIC, .bussdictEntry .STRONG, .bussdictEntry .COLLEXA, .bussdictEntry .THESEXA, .bussdictEntry .GOODCOLLO, .bussdictEntry .SECHEADING, .bussdictEntry .subheading { font-style: italic; } .ldoceEntry .italic, .ldoceEntry .infllab, .bussdictEntry .italic, .bussdictEntry .infllab { font-style: italic; font-weight: normal; } .bussdictEntry .SECHEADING, .bussdictEntry .subheading, .bussdictEntry .UNDERLINE { text-decoration: underline; } .ldoceEntry .OBJECT { font-weight: normal; } .ldoceEntry .synopp, .ldoceEntry .FREQ, .ldoceEntry .AC { display: inline-block; font-style: normal; text-transform: uppercase; border-radius: 5px; border: solid 1px; padding-left: 4px; padding-right: 4px; } .longman_dict .GramBox .boxheader, .longman_dict .ThesBox .heading, .longman_dict .ColloBox .heading { line-height: 2em; } .longman_dict .ColloBox .heading { margin: 0 10px 0 0; } .ldoceEntry .Collocate, .ldoceEntry .Exponent { margin: 15px 0 0 5px; } .ldoceEntry .BADCOLLO { text-decoration: line-through; } .bussdictEntry .DERIV { font-size: 120%; } .bussdictEntry .HYPHENATION { font-size: 160%; } .bussdictEntry .OBJECT { font-weight: normal; } .bussdictEntry .REFHWD { text-transform: lowercase; font-style: normal; /*font-variant: small-caps;*/ } .bussdictEntry .neutral { font-style: normal; font-weight: normal; font-variant: normal; } .bussdictEntry .sensenum { font-style: normal; margin-right: 5px; margin-left: 3px; } .bussdictEntry .gramref { margin: 5px 0; } .bussdictEntry .BADCOLLO, .bussdictEntry .BADEXA { text-decoration: line-through; } .verbTable td { padding: 0 10px; } .verbTable .col1 { color: #333333; padding: 20px 10px 0; font-size: 14px; text-decoration: underline } .verbTable .view_more span, .verbTable .view_less span { cursor: pointer; } .verbTable .header { background-color: #333333; color: #fff; padding: 0 0 0 10px; font-size: 120%; } .longman_dict .wordfams .crossRef, .longman_dict .wordfams .w { margin: 0 6px; } .longman_dict .asset { margin-top: 20px; } .longman_dict .Entry .topics_container { display: table; } ================================================ FILE: src/css/main.css ================================================ * { box-sizing: border-box; outline-color: #448aff; } html, body, p, h1, h2, h3, h4, h5, h6, ul, li { margin: 0; padding: 0; } ul, li { list-style: none; } body { font-family: Roboto, 'Segoe UI', Arial, 'Microsoft Yahei', sans-serif; /*font-family: Helvetica Neue, PingFang SC, Microsoft YaHei, sans-serif;*/ background: #f5f8fa; color: #333; line-height: 1.8; } ::-webkit-scrollbar { width: 5px; height: 5px; } ::-webkit-scrollbar-track { /*background-color: #E2E4E6;*/ } ::-webkit-scrollbar-thumb { background-color: #bdc3c7; border-radius: 5px; } ::-webkit-scrollbar-thumb:hover { background-color: #9da5ac; } ::-webkit-scrollbar-thumb:active { background-color: #2c3e50; } a { text-decoration: none; color: #576b95; } a:hover { color: #1a73e8; } .fx:after { display: block; content: ''; clear: both; } .dmx_hide { display: none !important; } .dmx_show { display: block !important; } .mt_1 { margin-top: 10px; } .mt_2 { margin-top: 20px; } .ml_1 { margin-left: 10px; } .ml_2 { margin-left: 20px; } .main { margin: 0 auto; padding: 10px; min-width: 300px; font-size: 15px; } label { position: relative; display: inline-block; margin-right: 10px; line-height: 1.6; /*user-select: none;*/ } label u { text-decoration: none; margin-left: 20px; color: #a2a2a2; font-size: 14px; } .label_block label { display: block; margin: 0; } input[type=radio], input[type=checkbox] { width: 18px; height: 18px; margin: 0 5px 0 5px; position: relative; bottom: -4px; } /* Firefox */ @-moz-document url-prefix() { input[type=radio], input[type=checkbox] { bottom: auto; } } select { min-width: 100px; max-width: 160px; height: 30px; padding: 3px; font-size: 15px; border: 1px solid #cfd0d2; border-radius: 4px; } .dmx_button { display: inline-block; background: #4395ff; border: 1px solid #3589ff; border-radius: 4px; color: #fff; line-height: 1; padding: 8px 12px; font-size: 14px; white-space: nowrap; text-align: center; outline: none; box-sizing: border-box; user-select: none; cursor: pointer; transition: .1s; } .dmx_button:hover { box-shadow: 2px 2px 5px 0 #a6aab0; } .dmx_button:active { opacity: .7; } .dmx_button.medium { font-size: 105%; padding: 10px 13px; } .dmx_button.big { font-size: 125%; padding: 10px 16px; } .dmx_button .dmx-icon { margin-right: 5px; } .dmx_button_default { background: #fff; border: 1px solid #dcdfe6; color: #606266; } .dmx_button_success { background-color: #67c23a; border-color: #67c23a; color: #fff; } .dmx_button_warning { background-color: #e6a23c; border-color: #e6a23c; color: #fff; } .dmx_button_danger { background-color: #ef473a; border-color: #dd1419; color: #fff; } .dmx_button:disabled, .dmx_button:disabled:hover { cursor: not-allowed; opacity: .3; box-shadow: none; } .divider { position: relative; text-align: center; margin: 10px 0; } .divider:after { position: absolute; top: 50%; display: block; content: ''; width: 100%; border-bottom: 1px solid #b9b9b9; } .divider b { z-index: 1; display: inline-block; padding: 0 10px; position: relative; background: #F1F4F6; font-size: 120%; font-weight: 400; } .item_input, .item_textarea { background: #fff; border-radius: 4px; border: 1px solid #dcdfe6; box-sizing: border-box; color: #606266; display: inline-block; height: 40px; line-height: 40px; outline: none; font-size: 16px; padding: 0 15px; transition: border-color .2s cubic-bezier(.645, .045, .355, 1); width: 100%; } .item_input:hover, .item_textarea:hover { border-color: #c0c4cc; } .item_input:focus, .item_textarea:focus { border-color: #409eff; } .item_textarea { padding: 5px 15px; height: auto; min-height: 42px; line-height: 26px; resize: vertical; } .dmx_form_item { display: flex; flex-wrap: nowrap; align-items: center; justify-content: flex-start; margin-top: 10px; } .dmx_form_item .item_label { padding: 0 10px 0 0; color: #606266; font-size: 16px; box-sizing: border-box; white-space: nowrap; } .dmx_form_item .item_content { width: 100%; } .dmx_form_item .item_content.number { width: 100px; } .dmx_form_item .item_content.number input[type="number"] { padding-right: 2px; } .dmx_form_item.small .item_label { color: #333; font-size: 14px; } .dmx_form_item.small .item_input { height: 32px; line-height: 32px; font-size: 14px; padding: 0 8px; } /* ==================== player ==================== */ .dmx_player { margin-top: 10px; position: relative; width: 100%; height: 120px; max-width: 920px; background: #d7dde8; border-radius: 5px; } .dmx_player .dmx_surfer { width: 100%; height: calc(100% - 26px); overflow: hidden; border-radius: 0 0 5px 5px; } .dmx_player .dmx_controls { display: flex; position: absolute; top: 26px; left: 0; width: 100%; height: calc(100% - 26px); z-index: 999; } .dmx_player .dmx_p_top { background: #757F9A; color: #FFFFFF; height: 26px; line-height: 26px; padding: 0 10px; border-radius: 5px 5px 0 0; } .dmx_player .dmx_p_top .dmx_p_title { float: left; font-size: 16px; } .dmx_player .dmx_p_top .dmx_p_time { float: right; font-size: 14px; font-family: Consolas, serif; } .dmx_player .dmx_controls button { display: flex; justify-content: center; align-items: center; position: relative; top: 50%; left: 50%; min-width: 100px; height: 60px; white-space: nowrap; cursor: pointer; border-radius: 10px; outline: 0; padding: 0 10px; margin-top: -30px; margin-left: -50px; font-size: 32px; background-color: #FF4B2B; border-color: #FF4B2B; color: #FFF; } .dmx_player .dmx_controls button:hover { background: #f78989; border-color: #f78989; color: #FFF; } .dmx_player .dmx_controls button.disabled { cursor: not-allowed; background: #fab6b6; border-color: #fab6b6; color: #FFF; } .dmx_circle { display: flex; justify-content: center; align-items: center; height: 64px; width: 64px; position: relative; top: 50%; left: 50%; border-radius: 50%; background: linear-gradient(#e9eaeb, #D3CCE3); border: 1px solid #bdc3c7; box-shadow: 0 1px 3px rgba(0, 0, 0, .19), inset 0 1px 0 rgba(255, 255, 255, .4); transition: height 0.25s ease, width 0.25s ease; transform: translate(-50%, -50%); } .dmx_circle:hover { outline: none; } .dmx_circle:before, .dmx_circle:after { display: block; position: absolute; content: ""; top: 0; right: 0; bottom: 0; left: 0; border-radius: 50%; } .dmx_circle .dmx-icon { font-size: 46px; color: #333; } .dmx_circle.dmx_on { background: linear-gradient(#FF416C, #FF4B2B); border: 1px solid #FF4B2B; } .dmx_circle.dmx_on .dmx-icon { color: #fff; } .dmx_circle.dmx_on:before, .dmx_circle.dmx_on:after { border: 0.015625rem solid #FF4B2B; } .dmx_circle.dmx_on:before { animation: dmx_ripple 2s linear infinite; } .dmx_circle.dmx_on:after { animation: dmx_ripple 2s linear 1s infinite; } .dmx_reverse.dmx_on:before, .dmx_reverse.dmx_on:after { animation-direction: reverse; } .dmx_reverse.dmx_on { cursor: pointer; } @keyframes dmx_ripple { 0% { transform: scale(1); } 50% { transform: scale(1.5); opacity: 0.5; } 75% { transform: scale(1.75); opacity: 0.2; } 100% { transform: scale(1.95); opacity: 0; } } /* ==================== setting ==================== */ .tab u { display: block; float: left; text-decoration: none; background: #fff; border: 1px solid #D5D7DF; border-left: 0; padding: 6px 10px; line-height: 1; cursor: pointer; } .tab u:first-child { border-radius: 5px 0 0 5px; border-left: 1px solid #D5D7DF; } .tab u:last-child { border-radius: 0 5px 5px 0; } .tab u:hover { color: #1a73e8; } .tab u.active { color: #fff; background-color: #409eff; border-color: #409eff; } .setting_box { padding: 0 3px; display: none; } .setting_option { margin-top: 15px; } .setting_option .option_title { position: relative; } .setting_option .option_title:after { position: absolute; top: 50%; display: block; content: ''; width: 100%; border-bottom: 1px solid #b9b9b9; } .setting_option b { z-index: 1; position: relative; background: #F1F4F6; padding-right: 5px; } .setting_option .option_box { padding-top: 10px; } .setting_option .second_box { margin: 10px 0 0 5px; } .setting_option .dmx-icon-setting { margin-left: 5px; color: #7b848c; cursor: pointer; } .setting_dialog { display: none; z-index: 99; position: fixed; left: 0; top: 0; width: 100%; height: 100%; background: #f5f8fa; overflow: auto; padding: 10px; box-sizing: border-box; } #local_tts_dialog select { float: left; max-width: 180px; } #local_tts_reset_setting { margin-left: 220px; } .local_list_name { display: inline-block; float: left; min-width: 220px; text-align: right; padding-right: 10px; } .local_list_box { display: inline-block; } .lang_list { color: #9da5ac; } .lang_list_err { color: #FF416C; } .dialog_back { position: fixed; left: 10px; top: 2px; cursor: pointer; } .dialog_back .dmx-icon { font-size: 20px; } /* ==================== speak ==================== */ .speak_body { margin: 62px 10px 10px; } #speak_input, textarea { display: block; border-radius: 4px; border: 1px solid #cfd0d2; background: #fff; color: #4e5053; min-height: 88px; line-height: 22px; padding: 5px 10px; font-size: 16px; outline: none; overflow: auto; resize: vertical; } #search_list_dialog textarea { width: 100%; min-height: 280px; } #speak_input:focus { border-color: #409eff; box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.37); } .speak_box select { margin: 10px 10px 0 0; } #speak_button { float: right; } /* ==================== history ==================== */ .history_box { padding: 60px 10px 10px; } .history_box > .card { margin: 0 auto; min-width: 860px; max-width: 1200px; } /* ==================== more ==================== */ .more_list .dmx_button { float: left; margin: 0 10px 10px 0; } /* ==================== record ==================== */ .record_main { margin: 0 auto; padding: 0 10px 10px; min-width: 500px; max-width: 920px; } .dmx_center { margin: 20px 0; text-align: center; } .dmx_left { margin: 20px 0; text-align: left; } .dmx_right { margin: 20px 0; text-align: right; } .learn_points { margin-top: 10px; font-size: 16px; line-height: 1.8; color: #b3b3b3; } .learn_points .title { position: relative; } .learn_points .title:after { position: absolute; top: 50%; display: block; content: ''; width: 100%; border-bottom: 1px solid #b9b9b9; } .learn_points b { z-index: 1; position: relative; background: #F1F4F6; padding-right: 5px; } .learn_points .case { padding: 5px; } /* ==================== favorite ==================== */ .head { z-index: 2; position: fixed; top: 0; background: #F6F7F9; box-shadow: 0 1px 2px 0 rgba(60, 64, 67, .30), 0 2px 6px 2px rgba(60, 64, 67, .15); width: 100%; height: 50px; overflow: hidden; } .head .logo { float: left; color: #1a73e8; text-shadow: -1px -1px white, 1px 1px #175fbf; letter-spacing: 2px; font-size: x-large; font-weight: bold; height: 50px; line-height: 50px; margin: 0 15px; } .head .logo .dmx-icon { font-size: x-large; text-shadow: none; margin-right: 5px; } .head .nav { float: left; display: flex; align-items: center; margin-left: 25px; } .head .nav li { padding: 0 15px; } .head .nav li a { position: relative; display: inline-block; font-size: 16px; height: 50px; line-height: 50px; color: #8590a6; } .head .nav li a:hover, .head .nav li a.active { color: #121212; font-weight: 600; } .head .nav li a.active:after { position: absolute; right: 0; bottom: -1px; left: 0; height: 4px; background: #06f; content: ''; } .head .tool { float: right; margin-right: 10px; height: 50px; display: flex; align-items: center; } .head .tool li { padding: 0 10px; font-size: 15px; cursor: pointer; } .head .tool li .dmx-icon { margin-right: 5px; } .cate { z-index: 1; position: fixed; top: 50px; bottom: 0; width: 250px; background: #F3F6F9; border-right: 1px solid #AFB3B5; overflow: auto; } .cate .dmx-icon:hover, .dmx_hover .dmx-icon:hover { color: #06f; cursor: pointer; } #create_cate_but { position: fixed; top: 52px; left: 217px; } #create_cate_but .dmx-icon { font-size: 20px; } .cate ul { padding: 5px 0; } .cate ul li { line-height: 32px; font-size: 15px; padding: 0 10px 0 15px; cursor: pointer; } .cate ul li .dmx-icon { color: #9EA9B7; margin-right: 5px; } .cate ul li:hover { background: #EBECEE; } .cate ul li.active { background: #EBECEE; } .cate ul li.active a, .cate ul li.active .dmx-icon { color: #1a73e8; } .cate ul li.active .dmx-icon:before { content: "\e6c5"; } .favorite_box { min-width: 1000px; margin: 60px 10px 10px 260px; } .card { background: #FFFFFF; border: 1px solid #CCCFD0; border-radius: 10px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, .1); } .card_head { display: flex; justify-content: left; align-items: center; height: 56px; padding: 10px 15px; font-size: 18px; border-bottom: 1px solid #CCCFD0; } .card_head > div { margin-right: 15px; } .card_head .dmx_vis { visibility: hidden; } .card_head:hover .dmx_vis { visibility: visible; } .card_head .item_num { font-size: 14px; color: #b1b1b1; } .card_head .dmx_button { margin-left: 10px; } .card_head .extra { display: none; } .card_body { padding-bottom: 5px; } table.dmx_table { width: 100%; min-width: 800px; border-collapse: collapse; } table.dmx_table tr:hover td { background: #F2F5F9; } table.dmx_table th, table.dmx_table td { text-align: left; background: #fff; border-top: 1px solid #ddd; padding: 8px; word-break: break-all; font-size: 15px; } table.dmx_table th { background: #F5F6F9; } table.dmx_table thead th { border-top: none; } table.dmx_table .tb_checkbox { width: 44px; } table.dmx_table .tb_index { width: 80px; } table.dmx_table .tb_words { width: 240px; } table.dmx_table .tb_records, table.dmx_table .tb_days { width: 90px; } table.dmx_table .tb_date { width: 110px; } table.dmx_table .tb_operate { width: 200px; } table.dmx_table .tb_operate2 { width: 140px; } table.dmx_table .table_empty { padding: 15px 10px; text-align: center; /*font-weight: bold;*/ } .player_box { min-width: 500px; max-width: 920px; margin: 0 auto; } #player_sentence { font-size: 20px; margin-top: 10px; padding: 5px; } #player_sentence.hide { color: #F1F4F6; } #player_sentence.hide:hover { color: #333; } #player_sentence u, .tb_sentence u { text-decoration: none; color: red; } #player_sentence.hide u { color: inherit; } #player_sentence.hide:hover u { color: red; } /* ==================== dal ==================== */ .dal, .dal_bg, .ddi, .ddi_bg { position: fixed; top: 0; bottom: 0; left: 0; right: 0; } .dal_bg, .ddi_bg { z-index: 999999999998; background: rgba(0, 0, 0, .6); } .dal, .ddi { z-index: 999999999999; display: flex; align-items: center; justify-content: center; } .dal_modal, .ddi_modal { position: relative; width: 500px; background: #fff; border-radius: 5px; padding: 20px; font-size: 16px; max-height: 95%; overflow: auto; } .dal_text { margin: 15px 0; } .dal_text .dmx-icon { margin-right: 8px; font-size: 130%; } .dal_text .dmx-icon-info { color: #e6a23c; } .dal_text .dmx-icon-close { color: #D50007; } .dal_text .dmx-icon-success { color: #67c23a; } .dal_foot { width: 100%; display: flex; align-items: center; justify-content: flex-end; } .dal_foot .dmx_button { margin-left: 10px; } .ddi_dialog { width: 600px; padding: 0; } .ddi_dialog.fullscreen { width: 100%; height: 100%; max-height: 100%; border-radius: 0; background: #F1F4F6; } .ddi_head { width: 100%; height: 38px; line-height: 38px; padding-left: 10px; font-weight: bold; } .ddi_head .dmx-icon-close { display: block; width: 20px; height: 20px; line-height: 20px; position: absolute; top: 10px; right: 10px; color: #FC4A4A; font-size: 18px; } .ddi_head .dmx-icon:hover { opacity: .8; cursor: pointer; } .ddi_body { margin: 0 15px 10px; } .ddi_loading { color: #fff; font-size: 18px; text-align: center; } .ddi_loading_inner { border: 5px solid #bdc3c7; border-top: 5px solid #2c3e50; border-bottom: 3px solid #2c3e50; border-radius: 50%; display: block; width: 64px; height: 64px; margin: 0 auto; animation: spin 1.2s linear infinite; } .dmx-icon-loading { animation: spin 1.2s linear infinite; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } @media only screen and (max-width: 789px) { .dal_modal { width: 430px; } } ================================================ FILE: src/css/popup.css ================================================ html, body { margin: 0; padding: 0; overflow: hidden; background: #f5f8fa; } .dmx_popup { width: 520px; height: 500px; } .dmx_popup body { width: 520px; } ================================================ FILE: src/html/favorite.html ================================================ 我的收藏
0
移动
删除
句子 练习次数 练习天数 添加时间 操作
================================================ FILE: src/html/history.html ================================================ 历史记录
历史记录
0
删除
内容 时间 操作
================================================ FILE: src/html/more.html ================================================ More
翻译窗口
解除限制
我的收藏
历史记录
朗读助手
英语音标
日语假名
PDF阅读器
================================================ FILE: src/html/popup.html ================================================ 梦想翻译小助手 ================================================ FILE: src/html/record.html ================================================ 练习发音
练习 0
添加收藏
我的收藏
句子
生词
音频
返回
================================================ FILE: src/html/setting.html ================================================ Setting
划词功能
划词过滤
对话框位置
实用功能
恢复默认设置
启用翻译
自动朗读
本机朗读
替换朗读
截图识别
识别语言
识别引擎
百度云应用AK
百度云应用SK
更多功能
启用词典
自动朗读
朗读:
联系作者
如果您有任何建议和问题反馈,请邮件发送到 Dream39999@gmail.com
开发原因
每当看到一些容易误导人的背单词学习英语广告,文章,视频,软件,就实在忍不住想做点什么,于是就有了这款程序。
学习方法
如果你对「正确学习英语的方法」感兴趣,请阅读《英语学习秘籍》。
朗读速度
朗读音调
恢复默认设置
保存
返回
设置说明
每行一条,“|”分隔,前面为名称,后面为搜索链接,{0} 代表搜索关键字。
================================================ FILE: src/html/speak.html ================================================ 朗读助手
朗读
================================================ FILE: src/html/video.html ================================================ Video ================================================ FILE: src/js/background.js ================================================ 'use strict' /** * Dream Translate * https://github.com/ryanker/dream_translate * @Author Ryan * @license MIT License */ let conf, setting, sdk = {} let searchText, searchList let ocrToken = '', ocrExpires = 0 var textTmp = '' var historyMax = 3000 document.addEventListener('DOMContentLoaded', async function () { let languageList = '', dialogCSS = '', dictionaryCSS = {} await fetch('../conf/conf.json').then(r => r.json()).then(r => { conf = r }) await fetch('../conf/searchText.txt').then(r => r.text()).then(str => { searchText = str }) await fetch('../conf/language.json').then(r => r.text()).then(s => { languageList += s }) await fetch('../css/dmx_dialog.css').then(r => r.text()).then(s => { dialogCSS += minCss(s) }) for (let name of conf.dictionaryCSS) { await fetch(`../css/${name}.css`).then(r => r.text()).then(s => { dictionaryCSS[name] = minCss(s) }) } storageLocalSet({conf, languageList, dialogCSS, dictionaryCSS}).catch(err => debug(`save error: ${err}`)) await storageSyncGet(['setting', 'searchText']).then(function (r) { saveSettingAll(r.setting, true) // 初始设置参数 searchList = getSearchList(r.searchText || searchText) if (!r.searchText) saveSearchText('') // 如果为空,设置默认值 }) // 最大保存历史记录数 if (localStorage['historyMax']) historyMax = Number(localStorage['historyMax']) // 加载 js loadJs(uniqueArray(Object.keys(conf.translateList).concat(Object.keys(conf.translateTTSList))), 'translate') loadJs(Object.keys(conf.dictionaryList), 'dictionary') // 添加菜单 setting.searchMenus.forEach(name => { let url = searchList[name] url && addMenu(name, name, url) }) // 初始数据库 idb('favorite', 1, initFavorite) // 否则第一次安装时,"我的收藏"需要刷新一下才能正常看到数据。 // 查看全部数据 storageShowAll() }) // 添加上下文菜单 B.contextMenus.create({ title: "梦想翻译“%s”", contexts: ["selection"], onclick: function (info, tab) { if (!info.selectionText) return let msg = {action: 'contextMenus', text: info.selectionText} if (tab && tab.id > 0) { sendTabMessage(tab.id, msg) } else { getActiveTabId().then(tabId => sendTabMessage(tabId, msg)) } } }) // 监听消息 B.onMessage.addListener(function (m, sender, sendResponse) { sendResponse() debug('request:', m) debug('sender:', sender && sender.url ? sender.url : sender) let tabId = getJSONValue(sender, 'tab.id') if (!tabId) tabId = 'popup' if (m.action === 'translate') { createHistory(m) // 保存历史记录 runTranslate(tabId, m) } else if (m.action === 'translateTTS') { runTranslateTTS(tabId, m) } else if (m.action === 'dictionary') { runDictionary(tabId, m) } else if (m.action === 'playSound') { runPlaySound(tabId, m) } else if (m.action === 'menu') { changeMenu(m.name, m.isAdd) } else if (m.action === 'saveSetting') { saveSettingAll(m.setting, m.updateIcon, m.resetDialog) } else if (m.action === 'copy') { execCopy(m.text) // 后台复制,页面才不会失去焦点 } else if (m.action === 'transWindow') { openTransWindow() } else if (m.action === 'onRecord') { openRecord() } else if (m.action === 'openUrl') { openTab(m.url) } else if (m.action === 'onAllowSelect') { sendAllowSelect() } else if (m.action === 'onCropImg') { cropImageSendMsg() } else if (m.action === 'onSaveSearchText') { saveSearchText(m.searchText) } else if (m.action === 'onCapture') { setTimeout(_ => capturePic(sender.tab, m), 100) } else if (m.action === 'img2text') { getOcrText(tabId, m.base64).catch() } else if (m.action === 'textTmp') { textTmp = m.text // 划词文字缓存 } }) // 监听快捷键 B.commands.onCommand.addListener(function (command) { command = command + '' if (command === 'openWindow') { openTransWindow() } else if (command === 'cropImage') { cropImageSendMsg() } else if (command === 'stopPlayAudio') { stopAudio() } else if (command === 'clipboardTrans') { clipboardTrans() } else if (command === 'toggleScribble') { let scribbleTmp = window.scribbleTmp || 'off' if (['direct', 'clickIcon'].includes(setting.scribble)) scribbleTmp = 'off'; else if (setting.scribble === 'off' && scribbleTmp === 'off') scribbleTmp = 'direct'; [setting.scribble, scribbleTmp] = [scribbleTmp, setting.scribble] // 交换 window.scribbleTmp = scribbleTmp saveSettingAll(setting, true) // 保存 } }) async function runTranslate(tabId, m) { let {action, text, srcLan, tarLan} = m if (srcLan === 'auto') { srcLan = await autoLang(text) if (srcLan === tarLan) tarLan = srcLan === 'zh' ? 'en' : 'zh' } else if (setting.autoLanguage) { if (/\p{Script=Han}/u.test(text)) srcLan = 'zh' if (srcLan === tarLan) tarLan = srcLan === 'zh' ? 'en' : 'zh' } setting.translateList.forEach(name => { sdkInit(`${name}Translate`).then(sd => { sd.query(text, srcLan, tarLan).then(result => { debug(`${name}:`, result) sandFgMessage(tabId, {action, name, result}) }).catch(error => { sandFgMessage(tabId, {action, name, text, error}) }) // 链接 let link = sd.link(text, srcLan, tarLan) sandFgMessage(tabId, {action: 'link', type: action, name, link}) }) }) // 自动朗读 autoPlayTTS(tabId, text, srcLan).then(_ => null) } function runTranslateTTS(tabId, m) { let list = conf.translateList let tList = conf.translateTTSList let {name, type, text, lang} = m let message = {action: 'playSound', nav: 'translate', name, type, status: 'end'} playTTS(name === setting.localSoundReplace ? 'local' : name, text, lang).then(() => { sandFgMessage(tabId, message) }).catch(err => { debug(`${name} sound error:`, err) let errMsg = `${tList[name] ? tList[name] : list[name] + '朗读'}出错` sandFgMessage(tabId, Object.assign({}, message, {error: errMsg})) }) } function runDictionary(tabId, m) { let {action, text} = m window.dictionarySounds = {} // 返回的发音缓存 setting.dictionaryList.forEach(name => { sdkInit(`${name}Dictionary`).then(sd => { sd.query(text).then(result => { debug(`${name}:`, result) let {sound} = result if (sound && sound.length > 0) dictionarySounds[name] = sound // 记录发音 sandFgMessage(tabId, {action, name, result}) }).catch(error => { sandFgMessage(tabId, {action, name, text, error}) }) // 链接 sandFgMessage(tabId, {action: 'link', type: action, name, link: sd.link(text)}) }) }) // 自动朗读 setTimeout(() => { autoPlayAudio(tabId, text).then(_ => null) }, 300) } function runPlaySound(tabId, m) { let {action, nav, name, type, url} = m playAudio(url).then(() => { sandFgMessage(tabId, {action, nav, name, type, status: 'end'}) }).catch(err => { debug(`${name} sound error:`, err) let title = conf.dictionaryList[name] || conf.translateList[name] || '' sandFgMessage(tabId, {action, nav, name, type, error: `${title}发音出错`}) }) } function cropImageSendMsg() { getActiveTabId().then(tabId => tabId && sendTabMessage(tabId, {action: 'onCrop'})) } function capturePic(tab, m) { B.tabs.captureVisibleTab(tab.windowId, {}, function (data) { let im = document.createElement("img") im.onload = function () { let ca = document.createElement("canvas") ca.width = m.width ca.height = m.height let ca2d = ca.getContext("2d") let t = im.height / m.innerHeight ca2d.drawImage(im, m.startX * t, m.startY * t, m.width * t, m.height * t, 0, 0, m.width, m.height) let b = ca.toDataURL("image/jpeg") getOcrText(tab.id, b).catch() } im.src = data }) } // 图片文字识别 async function getOcrText(tabId, base64) { // 获取 token let access_token = '' await getOcrToken().then(token => { access_token = token }).catch(err => { sandFgMessage(tabId, {action: 'onAlert', message: err, type: 'error'}) }) if (!access_token) return // see https://cloud.baidu.com/doc/OCR/s/zk3h7xz52 let url = 'https://aip.baidubce.com/rest/2.0/ocr/v1/general_basic?access_token=' + access_token let b = base64.substr(base64.indexOf(",") + 1) let p = new URLSearchParams(`image=${encodeURIComponent(b)}&detect_language=true&language_type=${setting.translateOCR || 'CHN_ENG'}`) httpPost({url, body: p.toString()}).then(r => { let wordsRes = getJSONValue(r, 'words_result') if (wordsRes && wordsRes.length > 0) { let text = '' for (let v of wordsRes) text += v.words + '\n' sandFgMessage(tabId, {action: 'contextMenus', text: text.trim()}) } else { sandFgMessage(tabId, {action: 'onAlert', message: '百度图片识别失败', type: 'error'}) } }).catch(e => { sandFgMessage(tabId, {action: 'onAlert', message: '百度图片识别 API 出错', type: 'error'}) debug('baidu ocr error:', e) }) } function getOcrToken() { return new Promise((resolve, reject) => { if (localStorage['clearOcrExpires'] !== 'true') { localStorage['clearOcrExpires'] = 'false' ocrExpires = 0 } if (ocrExpires - 10 > getTimestamp()) return resolve(ocrToken) if (setting.ocrType === 'baidu') { let url = `https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=${setting.baidu_orc_ak}&client_secret=${setting.baidu_orc_sk}` httpGet(url, 'json').then(r => { if (r.expires_in > 0 && r.access_token) { ocrExpires = getTimestamp() + r.expires_in ocrToken = r.access_token resolve(ocrToken) } else if (r.error_description) { reject(r.error_description) } else { reject('请求百度接口网路错误') } }).catch(e => { debug('aip.baidubce.com api error!', e) reject('百度获取 TOKEN 接口错误') }) } else { httpGet('https://mengxiang.net/api/getBaiduOcrToken.json', 'json').then(r => { if (r.expires > 0 && r.token) { ocrExpires = r.expires ocrToken = r.token resolve(ocrToken) } else { reject('请求官方接口网路错误') } }).catch(e => { debug('mengxiang.net api error!', e) reject('获取免费 TOKEN 接口错误') }) } }) } function saveSearchText(s) { if (!s) s = searchText storageSyncSet({searchText: s}) searchList = getSearchList(s) } function saveSettingAll(data, updateIcon, resetDialog) { setting = Object.assign({}, conf.setting, data) updateIcon && changeBrowserIcon(setting.scribble) // 是否显示关闭划词图标 let options = resetDialog ? {setting, dialogConf: {}} : {setting} if (resetDialog) saveSearchText('') storageSyncSet(options) } function changeBrowserIcon(scribble) { B.browserAction.setIcon({path: `icon/128${scribble === 'off' ? '_off' : ''}.png`}) // setBrowserAction(scribble === 'off' ? 'OFF' : '') } function setBrowserAction(text) { B.browserAction.setBadgeText({text: text || ''}) B.browserAction.setBadgeBackgroundColor({color: 'red'}) isFirefox && B.browserAction.setBadgeTextColor({color: 'white'}) } function changeMenu(name, isAdd) { let url = searchList[name] if (url) isAdd ? addMenu(name, name, url) : removeMenu(name) } function addMenu(name, title, url) { // {type: "separator"} let mid = md5(name) B.contextMenus.create({ id: 'page_' + mid, title: title + '首页', contexts: ["page"], onclick: function () { B.tabs.create({url: (new URL(url)).origin}) } }) B.contextMenus.create({ id: 'selection_' + mid, title: title + "“%s”", contexts: ["selection"], onclick: function (info) { B.tabs.create({url: url.format(decodeURIComponent(info.selectionText))}) } }) } function removeMenu(name) { let mid = md5(name) B.contextMenus.remove('page_' + mid) B.contextMenus.remove('selection_' + mid) } function openTransWindow() { openWindow('trans', 600, 520, B.root + 'html/popup.html?fullscreen=1') } function clipboardTrans() { openWindow('trans', 600, 520, B.root + 'html/popup.html?fullscreen=1&clipboardRead=1', true) } function openRecord() { openWindow('record', 600, 520, B.root + 'html/record.html', true) } function openWindow(wid, width, height, url, reopen) { let name = `_window_${wid}` let openFn = function (width, height, left, top) { let o = {type: 'popup', width, height, url} // 居中 let screen = window.screen o.left = Math.floor(left > 0 ? left : (screen.width - o.width) / 2) o.top = Math.floor(top > 0 ? top : (screen.height - o.height) / 2) B.windows.create(o, w => window[name] = w.id) } let id = window[name] if (id) { B.windows.get(id, function (w) { if (!B.runtime.lastError && w.id) { if (reopen) { B.windows.remove(w.id) setTimeout(() => openFn(w.width, w.height, w.left, w.top), 100) } else { B.windows.update(w.id, {focused: true}) } } else { openFn(width, height) } }) } else { openFn(width, height) } } function openTab(url) { B.tabs.create({url}) } function sendAllowSelect() { getActiveTabId().then(tabId => { tabId && sendTabMessage(tabId, {action: 'allowSelect'}) }) } // 保存历史记录 function createHistory(m) { if (historyMax < 1) return // 如果为 0,则不再保存历史记录 let {text, formUrl, formTitle} = m // 空内容不保存 text = text.trim() if (!text) return // 排除和最后一次记录相同内容 if (window.lastHistory === text) return window.lastHistory = text // 排除扩展内查询 if (formUrl) { if (formUrl.indexOf(B.root + 'html/favorite.html') === 0) return if (formUrl.indexOf(B.root + 'html/history.html') === 0) return } idb('history', 1, initHistory).then(db => { db.create('history', { content: text, formTitle: formTitle || '', formUrl: formUrl || '', createDate: new Date().toJSON(), }).then(() => { // 清理早期数据 db.count('history').then(n => { if (n <= historyMax) return db.find('history', {direction: 'prev', offset: historyMax}).then(arr => { for (let v of arr) db.delete('history', v.id) }) }) }).catch(e => { debug('history create error:', e) }) }) } // 历史记录设置 function settingHistory(n) { historyMax = Number(n) localStorage.setItem('historyMax', n) } function minCss(s) { s = s.replace(/\/\*.*?\*\//g, '') s = s.replace(/\s+/g, ' ') s = s.replace(/\s*([:;{}!,])\s*/g, '$1') s = s.replace(/;}/g, '}') s = s.replace(/;}/g, '}') return s } async function autoLang(text) { let lang = 'en' // 默认值 await httpPost({ url: `https://fanyi.baidu.com/langdetect`, body: `query=${encodeURI(text)}` }).then(r => { if (r && r.lan) lang = r.lan }).catch(err => { debug(err) }) return lang } async function autoPlayTTS(tabId, text, lang) { let list = conf.translateTTSList || {} let arr = setting.translateTTSList || [] if (lang === 'auto') lang = await autoLang(text) for (let name of arr) { let message = {action: 'playSound', nav: 'translate', name, type: 'source', status: 'end'} await sandFgMessage(tabId, Object.assign({}, message, {status: 'start'})) await playTTS(name, text, lang).then(() => { sandFgMessage(tabId, message) }).catch(err => { debug(`${name} sound error:`, err) sandFgMessage(tabId, Object.assign({}, message, {error: `${list[name] || '发音'}出错`})) }) } } function playTTS(name, text, lang) { return new Promise((resolve, reject) => { stopAudio() sdkInit(`${name}Translate`).then(sd => { sd.tts(text, lang).then(val => { if (name === 'local') return resolve() if (Array.isArray(val)) { (async function () { let ok = false let err = new Error() for (let i = 0; i < val.length; i++) { await playAudio(val[i]).then(() => { if (!ok) ok = true // 为更好的兼容,只要有一次播放成功就算播放成功 }).catch(e => { err = e }) } ok ? resolve() : reject(err) })() } else { playAudio(val).then(() => { resolve() }).catch(err => { reject(err) }) } }).catch(err => { reject(err) }) }) }) } async function autoPlayAudio(tabId, text) { let list = conf.dictionaryList || {} let sounds = window.dictionarySounds let type = setting.dictionaryReader || 'us' let arr = setting.dictionarySoundList || [] for (let name of arr) { let message = {action: 'playSound', nav: 'dictionary', name, type, status: 'end'} await sandFgMessage(tabId, Object.assign({}, message, {status: 'start'})) // 显示开始朗读图标 let url = '' if (sounds[name]) { url = getSoundUrl(sounds[name], type) // 缓存中获取 } else { let sdkRun = {} await sdkInit(`${name}Dictionary`).then(r => { sdkRun = r }) await sdkRun.query(text).then(r => { if (r.sound) url = getSoundUrl(r.sound, type) // 接口中获取 }).catch(error => { debug(`${name} dictionary error:`, error) }) } debug('_playAudio_', name, type, url) if (!url) continue // 没有发音跳过 // 播放声音 await playAudio(url).then(() => { sandFgMessage(tabId, message) }).catch(err => { debug(`${name} sound error:`, err) sandFgMessage(tabId, Object.assign({}, message, {error: `${list[name] || ''}发音出错`})) }) } debug('_playAudio_ finish.') } function getSoundUrl(arr, type) { for (let v of arr) if (v.type === type && !v.isWoman) return v.url return '' } function stopAudio() { let a = window._Audio if (a) a.pause() if (B.tts && B.tts.stop) B.tts.stop() } function playAudio(url) { return new Promise((resolve, reject) => { if (!window._Audio) window._Audio = new Audio() let a = window._Audio let blobUrl = null if (typeof url === 'string') { a.src = url } else if (typeof url === 'object') { blobUrl = URL.createObjectURL(url) a.src = blobUrl } else { return reject('Audio url error:', url) } a.onended = function () { // if (blobUrl) URL.revokeObjectURL(blobUrl) // 释放内存 resolve() let url = a.src // 记录最后一次播放的链接 window.audioSrc = {url} getAudioBlob(url).then(b => window.audioSrc.blob = b) } a.onerror = function (err) { reject(err) } let playPromise = a.play() if (playPromise !== undefined) { playPromise.catch(err => { // reject(err) resolve() }) } }) } // 缓存音频文件 async function getAudioBlob(url, retry) { let b retry = retry || 3 for (let i = 0; i < retry; i++) { await httpGet(url, 'blob').then(blob => { // console.log(blob) b = blob }).catch(err => { console.warn('httpGet:' + err) }) if (b) return b } return null } function sdkInit(name) { return new Promise((resolve, reject) => { if (sdk[name]) return resolve(sdk[name]) if (typeof window[name] === 'function') { sdk[name] = new window[name]().init() resolve(sdk[name]) } else { let err = name + ' not exist!' debug('sdkInit error:', err) reject(err) } }) } function loadJs(arr, type) { arr.forEach(k => { let el = document.createElement("script") el.type = 'text/javascript' el.src = `/js/${type || 'translate'}/${k}.js` document.head.appendChild(el) }) } function invertObject(obj) { let r = {} for (const [key, value] of Object.entries(obj)) { r[value] = key } return r } // 清理元素属性 function cleanAttr(el, attrs) { el.querySelectorAll('*').forEach(e => { for (let i = e.attributes.length - 1; i >= 0; i--) { let v = e.attributes[i] if (typeof attrs === 'object') { if (!attrs.includes(v.name)) e.removeAttribute(v.name) // 过滤白名单 } else { e.removeAttribute(v.name) // 全部删除 } } }) } // 检测返回结果是否正确,如果不正确,则重试 async function checkRetry(callback, times) { times = times || 3 // 默认 3 次 let isOk = false let p for (let i = 0; i < times; i++) { p = callback(i) await p.then(r => { if (r.data && r.data.length > 0) isOk = true }).catch(_ => null) if (isOk) return p await sleep(300) } return p } /*function openBgPage(id, url, timeout) { isFirefox ? openIframe(id, url, timeout) : openPopup(id, url, timeout) } function removeBgPage(id) { isFirefox ? removeIframe(id) : removePopup(id) } // 打开一个几乎不可见的 popup (此方法只在 macOS 下有用,在 windows 系统下无效,firefox 浏览器也无效) function openPopup(id, url, timeout) { timeout = timeout || 20 * 1000 // 默认 20 秒 removePopup(id) let popupId = `_popup_${id || 'one'}` B.windows.create({type: 'popup', focused: false, width: 1, height: 1, url}, w => window[popupId] = w.id) // 定时关闭窗口,减少内存占用 _setTimeout(id, () => { removePopup(id) cleanPopup(url) // 清理所有小窗口 }, timeout) } function removePopup(id) { let popupId = `_popup_${id || 'one'}` let wid = window[popupId] if (!wid) return B.windows.remove(wid, () => B.runtime.lastError) // 关闭 window[popupId] = null } function cleanPopup(url) { B.windows.getAll({populate: true}, function (windows) { let curOri = new URL(url).origin windows.forEach(w => { if (w.type === 'popup' && w.width === 1 && w.tabs.length === 1) { // console.log('url:', w.tabs[0].url) if (!w.tabs[0].url || new URL(w.tabs[0].url).origin === curOri) { B.windows.remove(w.id, () => B.runtime.lastError) // 关闭 } } }) }) }*/ // 创建一个临时标签 function createTmpTab(id, url, timeout) { let tabName = `_tmp_tab_${id || 'one'}` removeTmpTab(id) // 关闭 B.tabs.create({active: false, url}, tab => window[tabName] = tab.id) if (timeout > 1000) _setTimeout(id, () => removeTmpTab(id), timeout) // 定时关闭窗口,减少内存占用 } // 关闭临时标签 function removeTmpTab(id) { let tabName = `_tmp_tab_${id || 'one'}` let tabId = window[tabName] if (!tabId) return B.tabs.remove(tabId, () => B.runtime.lastError) // 关闭 window[tabName] = null } // 强制刷新临时标签 function reloadTmpTab(id) { let tabName = `_tmp_tab_${id || 'one'}` let tabId = window[tabName] if (tabId) B.tabs.reload(tabId, {bypassCache: true}, () => B.runtime.lastError) // 重新加载 } function openIframe(id, url, timeout) { timeout = timeout || 20 * 1000 // 默认 20 秒 let ifrId = `_iframe_${id || 'one'}` let el = document.getElementById(ifrId) if (!el) { el = document.createElement('iframe') el.id = ifrId el.src = url document.body.appendChild(el) } else { el.src = url } // 定时删除,减小内存占用 _setTimeout(id, () => el && el.remove(), timeout) return el } function removeIframe(id) { let ifrId = `_iframe_${id || 'one'}` let el = document.getElementById(ifrId) el && el.remove() } function sliceStr(text, maxLen) { let r = [] if (text.length <= maxLen) { r.push(text) } else { // 根据优先级截取字符串,详细符号见:https://zh.wikipedia.org/wiki/%E6%A0%87%E7%82%B9%E7%AC%A6%E5%8F%B7 let separators = `?!;.-…,/"` separators += `?!;。--_~﹏·,:、` separators += `“”﹃﹄「」﹁﹂『』﹃﹄()[]〔〕【】《》〈〉()[]{}` let separatorArr = [...separators] let arr = text.split('\n') arr.forEach(s => { s = s.trim() if (!s) return if (s.length <= maxLen) { r.push(s) } else { do { if (s.length <= maxLen) { r.push(s) break } let end = false for (let i = 0; i < separatorArr.length; i++) { if (i + 1 === separatorArr.length) end = true let symbol = separatorArr[i] let n = s.indexOf(symbol) if (n === -1) continue if (n > maxLen) continue let s2 = s.substring(0, n).trim() s2 && r.push(s2) s = s.substring(n + 1).trim() break } if (!end) continue if (!s) break if (s.length <= maxLen) { r.push(s) break } let s1 = s.substring(0, maxLen) let s2 = s.substring(maxLen) let n = s1.lastIndexOf(' ') if (n !== -1) { // 处理英文 let s3 = s1.substring(0, n) let s4 = s1.substring(n) r.push(s3) s = (s4 + s2).trim() } else { // 没有空格,就硬切(这种情况一般是中文) r.push(s1) s = s2 } } while (s) } }) } return r } ================================================ FILE: src/js/common.js ================================================ 'use strict' /** * Dream Translate * https://github.com/ryanker/dream_translate * @Author Ryan * @license MIT License */ /*! * 浏览器统一兼容 * 参考: * https://github.com/mozilla/webextension-polyfill * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Chrome_incompatibilities * https://developer.chrome.com/docs/extensions/reference/ * https://crxdoc-zh.appspot.com/extensions/ */ const isDebug = true window.isFirefox = navigator.userAgent.includes("Firefox") // window.isFirefox = typeof browser !== "undefined" && Object.getPrototypeOf(browser) === Object.prototype const B = { extension: chrome.extension, getBackgroundPage: chrome.extension.getBackgroundPage, windows: chrome.windows, commands: chrome.commands, runtime: chrome.runtime, id: chrome.runtime.id, root: chrome.runtime.getURL(''), onMessage: chrome.runtime.onMessage, sendMessage: chrome.runtime.sendMessage, storage: chrome.storage, browserAction: chrome.browserAction, contextMenus: chrome.contextMenus, webRequest: chrome.webRequest, cookies: chrome.cookies, tabs: chrome.tabs, tts: chrome.tts, app: chrome.app, } String.prototype.format = function () { let args = arguments return this.replace(/{(\d+)}/g, function (match, number) { return typeof args[number] != 'undefined' ? args[number] : match }) } function storageLocalGet(options) { return storage('local', 'get', options) } function storageLocalSet(options) { return storage('local', 'set', options) } function storageSyncGet(options) { return storage('sync', 'get', options) } function storageSyncSet(options) { return storage('sync', 'set', options) } function storageShowAll() { if (!isDebug) return !isFirefox && storageSyncGet(null).then(function (r) { debug(`all sync storage:`, r) }) storageLocalGet(null).then(function (r) { debug(`all local storage:`, r) }) } function storage(type, method, options) { return new Promise((resolve, reject) => { if (!isFirefox) { if (typeof B.app.isInstalled === 'undefined') return reject('The extension has been updated!') let callback = function (r) { let err = B.runtime.lastError err ? reject(err) : resolve(r) } let api = type === 'sync' ? B.storage.sync : B.storage.local if (method === 'get') { api.get(options, callback) } else if (method === 'set') { api.set(options, callback) } } else { let api = isDebug ? browser.storage.local : type === 'sync' ? browser.storage.sync : browser.storage.local if (method === 'get') { api.get(options).then(r => resolve(r), err => reject(err)) } else if (method === 'set') { api.set(options).then(r => resolve(r), err => reject(err)) } } }) } function cookies(method, options) { return new Promise((resolve, reject) => { if (!isFirefox) { if (typeof B.app.isInstalled === 'undefined') return reject('The extension has been updated!') let callback = function (r) { let err = B.runtime.lastError err ? reject(err) : resolve(r) } if (method === 'get') { B.cookies.get(options, callback) } else if (method === 'getAll') { B.cookies.getAll(options, callback) } else if (method === 'set') { B.cookies.set(options, callback) } else if (method === 'remove') { B.cookies.remove(options, callback) } } else { if (method === 'get') { browser.cookies.get(options).then(r => resolve(r), err => reject(err)) } else if (method === 'getAll') { browser.cookies.getAll(options).then(r => resolve(r), err => reject(err)) } else if (method === 'set') { browser.cookies.set(options).then(r => resolve(r), err => reject(err)) } else if (method === 'remove') { browser.cookies.remove(options).then(r => resolve(r), err => reject(err)) } } }) } function sendMessage(message) { return new Promise((resolve, reject) => { if (!isFirefox) { if (typeof B.app.isInstalled === 'undefined') return reject('The extension has been updated!') B.sendMessage(message, r => B.runtime.lastError ? reject(B.runtime.lastError) : resolve(r)) } else { browser.runtime.sendMessage(message).then(r => resolve(r), err => reject(err)) } }) } function sendTabMessage(tabId, message) { return new Promise((resolve, reject) => { if (!isFirefox) { if (typeof B.app.isInstalled === 'undefined') return reject('The extension has been updated!') tabId && B.tabs.sendMessage(tabId, message, r => B.runtime.lastError ? reject(B.runtime.lastError) : resolve(r)) } else { tabId && browser.tabs.sendMessage(tabId, message).catch(err => debug('send error:', err)) } resolve() }) } function sandFgMessage(id, message) { if (id === 'popup') { let popup = B.extension.getViews({type: 'popup'}) if (popup.length > 0) { return sendMessage(message) } else { return Promise.resolve() } } else { return sendTabMessage(id, message) } } function getActiveTabId() { return new Promise((resolve, reject) => { if (!isFirefox) { B.tabs.query({currentWindow: true, active: true}, tab => { resolve(getJSONValue(tab, '0.id')) // todo: 还有优化空间 }) } else { browser.tabs.query({currentWindow: true, active: true}).then(tab => { let tabId = tab[0] && resolve(tab[0].id) resolve(tabId) }, err => reject(err)) } }) } function onBeforeSendHeadersAddListener(callback, filter, opt_extraInfoSpec) { if (!opt_extraInfoSpec) opt_extraInfoSpec = Object.values(B.webRequest.OnBeforeSendHeadersOptions) B.webRequest.onBeforeSendHeaders.addListener(callback, filter, opt_extraInfoSpec) } function onBeforeSendHeadersRemoveListener(callback) { B.webRequest.onBeforeSendHeaders.removeListener(callback) } function requestHeadersFormat(s) { let r = [] let arr = s.split('\n') arr && arr.forEach(v => { v = v.trim() if (!v) return let a = v.split(': ') if (a.length === 2) r.push({name: a[0].trim(), value: a[1].trim()}) }) return r } function onBeforeRequestAddListener(callback, filter, extraInfoSpec) { if (!extraInfoSpec) { extraInfoSpec = ["blocking", "extraHeaders", "requestBody"] // 解决 chrome 审核机制太垃圾,提示没有使用到 webRequestBlocking extraInfoSpec = Object.values(B.webRequest.OnBeforeRequestOptions) } B.webRequest.onBeforeRequest.addListener(callback, filter, extraInfoSpec) } function onBeforeRequestRemoveListener(callback) { B.webRequest.onBeforeRequest.removeListener(callback) } function onHeadersReceivedAddListener(callback, filter, extraInfoSpec) { if (!extraInfoSpec) extraInfoSpec = Object.values(B.webRequest.OnHeadersReceivedOptions) B.webRequest.onHeadersReceived.addListener(callback, filter, extraInfoSpec) } function onHeadersReceivedRemoveListener(callback) { B.webRequest.onHeadersReceived.removeListener(callback) } function onCompletedAddListener(callback, filter, extraInfoSpec) { if (!extraInfoSpec) extraInfoSpec = Object.values(B.webRequest.OnCompletedOptions) B.webRequest.onCompleted.addListener(callback, filter, extraInfoSpec) } function onCompletedRemoveListener(callback) { B.webRequest.onCompleted.removeListener(callback) } function onRemoveFrame(details) { let headers = Object.assign([], details.responseHeaders) for (let i = 0; i < headers.length; i++) { let name = headers[i].name.toLowerCase() if (name.includes('frame-options') || name.includes('content-security-policy')) { headers.splice(i, 1) // break } } return {responseHeaders: headers} } function onRemoveCross(details) { let headers = Object.assign([], details.responseHeaders) for (let i = 0; i < headers.length; i++) { let name = headers[i].name.toLowerCase() if (name.includes('cross-origin-resource-policy') || name.includes('cross-origin-opener-policy')) { headers.splice(i, 1) // break } } return {responseHeaders: headers} } // 获得所有语音的列表 (firefox 不支持) function getVoices() { return new Promise((resolve, reject) => { if (!B.tts || !B.tts.getVoices) return reject("I won't support it!") B.tts.getVoices(function (voices) { let list = {} for (let i = 0; i < voices.length; i++) { // debug('Voice ' + i + ':', JSON.stringify(voices[i])) let v = voices[i] if (!list[v.lang]) list[v.lang] = [] list[v.lang].push({lang: v.lang, voiceName: v.voiceName, remote: v.remote}) } resolve(list) }) }) } function getTimestamp() { return Date.parse(new Date() + '') / 1000 } function addClass(el, className) { if (!el || !className) return className = className.trim() let oldClassName = el.className.trim() if (!oldClassName) { el.className = className } else if (` ${oldClassName} `.indexOf(` ${className} `) === -1) { el.className += ' ' + className } } function rmClass(el, className) { if (!el.className) return className = className.trim() let newClassName = el.className.trim() if ((` ${newClassName} `).indexOf(` ${className} `) === -1) return newClassName = newClassName.replace(new RegExp('(?:^|\\s)' + className + '(?:\\s|$)', 'g'), ' ').trim() if (newClassName) { el.className = newClassName } else { el.removeAttribute('class') } } function hasClass(el, className) { if (!el.className) return false return (` ${el.className.trim()} `).indexOf(` ${className.trim()} `) > -1 } function sleep(delay) { return new Promise(r => setTimeout(r, delay)) } function getDate(value, isDate) { let d = value ? new Date(value) : new Date() d.setMinutes(-d.getTimezoneOffset() + d.getMinutes(), d.getSeconds(), 0) let s = d.toISOString() if (isDate) { s = s.substring(0, 10) } else { s = s.replace('T', ' ') s = s.replace('.000Z', '') } return s } // 补零 function zero(value, digits) { digits = digits || 2 let isNegative = Number(value) < 0 let s = value.toString() if (isNegative) s = s.slice(1) let size = digits - s.length + 1 s = new Array(size).join('0').concat(s) return (isNegative ? '-' : '') + s } function $(id) { return document.getElementById(id) } function N(name) { return document.getElementsByName(name) } function S(s) { return document.querySelector(s) } function D(s) { return document.querySelectorAll(s) } function onD(el, type, listener, options) { el.forEach(v => { v.addEventListener(type, listener, options) }) } function unD(el, type, listener, options) { el.forEach(v => { v.removeEventListener(type, listener, options) }) } function removeD(el) { el.forEach(e => e.remove()) } function rmClassD(el, className) { el.forEach(v => rmClass(v, className)) } function inArray(val, arr) { // return arr.indexOf(val) !== -1 return arr.includes(val) } function isObject(o) { return Object.prototype.toString.call(o) === '[object Object]' } function isArray(o) { return Object.prototype.toString.call(o) === '[object Array]' } function isString(o) { return Object.prototype.toString.call(o) === '[object String]' } function isNumber(o) { return Object.prototype.toString.call(o) === '[object Number]' } function isDate(o) { return Object.prototype.toString.call(o) === '[object Date]' } function isRegExp(o) { return Object.prototype.toString.call(o) === '[object RegExp]' } function isError(o) { return Object.prototype.toString.call(o) === '[object Error]' } function isSymbol(o) { return Object.prototype.toString.call(o) === '[object Symbol]' } function isArrayBuffer(o) { return Object.prototype.toString.call(o) === '[object ArrayBuffer]' } function isFunction(o) { return Object.prototype.toString.call(o) === '[object Function]' } function getSearchList(s) { s = s.trim() let arr = s.split('\n') let r = {} for (let v of arr) { v = v.trim() let a = v.split('|') let key = a[0] && a[0].trim() let val = a[1] && a[1].trim() if (key && val) r[key] = val } return r } // 解决 JSON 太深问题 function getJSONValue(data, keys, value) { // if (!data || !isObject(data)) return value // 默认值 if (!data) return value // 默认值 keys = keys.trim() let arr = keys.split('.') let val = Object.assign({}, data) for (let key of arr) { if (!val[key]) return value // 默认值 val = val[key] } return val } // 添加 DOM 元素 function addEl(options) { let {tagName, id, className, text, title, onClick} = options let el = document.createElement(tagName) if (id) el.id = id if (className) el.className = className if (text) el.textContent = text if (title) el.title = title if (onClick) el.addEventListener('click', onClick) return el } function createTextarea() { let t = document.createElement("textarea") t.style.position = 'fixed' t.style.top = '-200%' document.body.appendChild(t) return t } function execCopy(s) { let t = createTextarea() t.value = s t.select() document.execCommand("copy") document.body.removeChild(t) } function execPaste() { let t = createTextarea() t.focus() document.execCommand("paste") let v = t.value document.body.removeChild(t) return v } // dream alert function dal(text, type, onSubmit) { let icon = { info: '', error: '', success: '', } D('.dal_bg,.dal').forEach(e => e.remove()) // 只允许存在一个 document.body.insertAdjacentHTML('beforeend', `
${(icon[type] || icon.info) + text}
`) let rmFn = () => D('.dal_bg,.dal').forEach(e => e.remove()) S('[data-type="submit"]').addEventListener('click', rmFn) if (typeof onSubmit === 'function') S('[data-type="submit"]').addEventListener('click', onSubmit) } // dream confirm function dco(text, onSubmit, onCancel) { D('.dal_bg,.dal').forEach(e => e.remove()) // 只允许存在一个 document.body.insertAdjacentHTML('beforeend', `
${text}
`) let submitEl = S('[data-type="submit"]') let cancelEl = S('[data-type="cancel"]') let rmFn = () => D('.dal_bg,.dal').forEach(e => e.remove()) submitEl.addEventListener('click', rmFn) cancelEl.addEventListener('click', rmFn) if (typeof onSubmit === 'function') submitEl.addEventListener('click', onSubmit) if (typeof onCancel === 'function') cancelEl.addEventListener('click', onCancel) } // dream dialog function ddi(option) { let o = Object.assign({ title: '', body: '', fullscreen: false, onClose: null, }, option || {}) let el = S('.ddi .ddi_body') if (el) { el.innerHTML = o.body } else { document.body.insertAdjacentHTML('beforeend', `
${o.title}
${o.body}
`) addClass(document.body, 'dmx_overflow_hidden') S('.ddi_head .dmx-icon-close').addEventListener('click', () => { rmClass(document.body, 'dmx_overflow_hidden') removeDdi() if (typeof o.onClose === 'function') o.onClose() }) } } // remove dream dialog function removeDdi() { rmClass(document.body, 'dmx_overflow_hidden') D('.ddi_bg,.ddi').forEach(e => e.remove()) } function loading(text) { document.body.insertAdjacentHTML('beforeend', `
${text || 'loading...'}
`) } // 过滤 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 // 绑定事件 sList.forEach(name => { let caseEl = I(`${name}_translate_case`) let copyEl = caseEl.querySelector('.case_copy') let bilingualEl = caseEl.querySelector('.case_bilingual') let contentEl = caseEl.querySelector('.case_content') copyEl.addEventListener('click', () => { let text = contentEl.innerText.replace(/\n{2}/g, '\n').trim() execCopy(text) dmxAlert('复制成功', 'success') }) bilingualEl.addEventListener('click', () => { if (hasClass(bilingualEl, 'active')) { resultTranslate(name, false) rmClass(bilingualEl, 'active') } else { resultTranslate(name, true) addClass(bilingualEl, 'active') } }) }) } function loadingDictionary() { let el = I('case_list') let cList = conf.dictionaryList let sList = setting.dictionaryList 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 += `

${part}

` }) } else { partStr += 'Sorry,没有找到与此相符的结果' } } s += `
${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: `
分类
` }) $('move_cate_but').addEventListener('click', () => { let cateId = Number($('move_cateId').value) if (cateId < 0) return dal('请选择分类', 'error') let eList = D('td.tb_checkbox input[type="checkbox"]:checked') eList.forEach(el => { db.update('sentence', Number(el.value), {cateId}).catch(_ => dal('移动失败', 'error')) }) setTimeout(() => { removeDdi() initCate(cateId) selectCancel() }, 1000) }) }) }) } // 批量删除句子 function deleteBatchSentence() { $('sentence_delete').addEventListener('click', function () { let eList = D('td.tb_checkbox input[type="checkbox"]:checked') dco(`删除不可恢复,您确认要删除这 ${eList.length} 条数据吗?`, () => { eList.forEach(el => { db.delete('sentence', Number(el.value)).catch(_ => dal('删除失败', 'error')) }) setTimeout(() => { initCate() selectCancel() }, 1000) }) }) } // 加载分类 function initCate(id) { db.getAll('cate').then(arr => { let s = '' arr.forEach(v => { s += `
  • ${v.cateName}
  • ` }) $('cate_box').innerHTML = s // 分类筛选 D('#cate_box li').forEach(el => { el.addEventListener('click', () => { cateId = Number(el.dataset.id) initSentence(cateId) D('#cate_box li.active').forEach(e => rmClass(e, 'active')) addClass(el, 'active') }) }) // 初始 let firstEl = S(`#cate_box li[data-id="${typeof id === 'number' ? id : cateId || 0}"]`) if (firstEl) firstEl.click() }) } // 加载句子 function initSentence(cateId) { let thLen = D('#sentence_box thead th').length let tbodyEl = S('#sentence_box tbody') if (!tbodyEl.innerHTML) tbodyEl.innerHTML = `
    ` db.read('cate', cateId).then(cate => $('cate_name').innerText = cate.cateName) db.count('sentence', 'cateId', cateId).then(n => $('sentences').innerText = n) let orderBy = localStorage['orderBy'] let direction = orderBy === 'reverse' ? 'prev' : 'next' db.find('sentence', {indexName: 'cateId', query: cateId, direction}).then(arr => { if (arr.length < 1) { tbodyEl.innerHTML = `暂无内容` return } if (orderBy === 'random') shuffle(arr) // 随机 // console.log(JSON.stringify(arr)) let s = '' arr.forEach((v, k) => { s += ` ${pointSentence(v.sentence, v.words)} ${v.records} ${v.days} ${getDate(v.createDate, true)}
    练习
    修改
    删除
    ` }) tbodyEl.innerHTML = s sentenceData = arr selectBind() exerciseSentence() // 练习句子 editSentence() deleteSentence() }) } // 练习句子 function exerciseSentence() { D('.dmx_button[data-action="skill"]').forEach(el => { el.addEventListener('click', () => { ddi({ fullscreen: true, 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 = `
    ` db.count('history').then(n => $('historyNum').innerText = n) db.find('history', {direction: 'prev'}).then(arr => { if (arr.length < 1) { tbodyEl.innerHTML = `暂无内容` return } // console.log(JSON.stringify(arr)) let s = '' arr.forEach((v, k) => { s += ` ${HTMLEncode(v.content)} ${getDate(v.createDate, true)} 来源
    删除
    ` }) tbodyEl.innerHTML = s // 选中 let eList = D('#history_body 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') // 是否显示批量删除按钮 }) }) // 删除 D('.dmx_button[data-action="delete"]').forEach(el => { el.addEventListener('click', () => { let id = Number(el.parentNode.dataset.id) dco(`删除不可恢复,您确认要删除吗?`, () => { db.delete('history', id).then(_ => historyList()).catch(_ => dal('删除失败', 'error')) }) }) }) }) } // 批量删除 function deleteMultiple() { $('delete_multiple').addEventListener('click', function () { let eList = D('#history_body input[type="checkbox"]:checked') dco(`删除不可恢复,您确认要删除这 ${eList.length} 条数据吗?`, () => { eList.forEach(el => { db.delete('history', Number(el.value)).catch(_ => dal('删除失败', 'error')) }) setTimeout(_ => historyList(), 1000) // 取消 rmClass($('extra_but'), 'dmx_show') $('selectAll').checked = false }) }) } // 全选/取消全选 function selectAll() { $('selectAll').addEventListener('click', function () { let eList = D('#history_body input[type="checkbox"]') eList.forEach(el => el.checked = this.checked) ;(this.checked && eList.length > 0 ? addClass : rmClass)($('extra_but'), 'dmx_show') // 是否显示批量删除按钮 }) } // 设置 function openSetting() { $('setting').addEventListener('click', function () { ddi({ title: '设置', body: `
    最大记录数
    ` }) $('save_but').addEventListener('click', () => { let maxNum = $('history_max').value bg.settingHistory(maxNum) removeDdi() }) }) } ================================================ FILE: src/js/more.js ================================================ 'use strict' /** * Dream Translate * https://github.com/ryanker/dream_translate * @Author Ryan * @license MIT License */ document.addEventListener('DOMContentLoaded', function () { $('trans_window').addEventListener('click', () => sendMessage({action: 'transWindow'})) $('allow_select').addEventListener('click', () => { sendMessage({action: 'onAllowSelect'}).then(_ => { if (new URL(location.href).searchParams.get('isSome') === 'true') parent.window.close() }) }) D('[data-href]').forEach(e => e.addEventListener('click', () => { let url = e.dataset.href if (url.substr(0, 4) !== 'http') url = B.root + 'html/' + url sendMessage({action: 'openUrl', url}) })) if (isFirefox) S('[data-href="speak.html"]').remove() // Firefox 不支持这个功能 }) ================================================ FILE: src/js/player.js ================================================ 'use strict' /** * Dream Translate * https://github.com/ryanker/dream_translate * @Author Ryan * @license MIT License */ window.playerTips = `
    练习要点
    1、带上耳机听发音,更有助听清声音中的细节。
    2、练句子,把生词融入句子中。
    3、先理解句子含义。如有生词时,应先根据经验猜,然后才查词典确认猜的对不对。首选查英英词典,推荐朗文或格林斯之类的词典,把英语当工具使用,用英语思维去学习生词。如果还没达到这个能力,才考虑查中英词典。当理解含义后,忘掉所有文字,不管中文还是英文,理解含义为最终目的。
    4、认真听句子发音,并模仿发音。模仿发音时,需要忘掉所有文字,脑海里应浮现出句子运用的场景,把自己置身在场景中,投入感情和动用感官(视觉,听觉,嗅觉,味觉,触觉)去模仿。
    5、认真听自己的发音和原音的差异。
    6、重复第 4-5 步,自我修正发音。直到你感觉语速能跟上,发音接近,并且很流利为止。「练习次数多多益善」
    语速问题
    如果感觉语速跟不上,这证明缺乏锻炼,不要想着去降低播放速度,而需要鼓励自己,锻炼你的耳朵和嘴巴。开始练习发音吧,当你重复练习 N 次后,你会感觉语速变慢了。相信自己,你可以的!
    语言运用
    语言是技能,最重要的是实战运用。所以句子练熟之后呢?就需要去使用这些句子,如果你有个友好耐心的老外朋友当陪练那当然最好,没有也不用特别在意,在学习外语过程中,用外语跟自己说话,要比跟别人交流更重要。
    学习方法
    如果你对「正确学习英语的方法」感兴趣,请阅读《英语学习秘籍》。
    ` // 播放 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 = `
    ${preName}
    ${select}
    ` if (ttsKeys.includes(key) || specialLang.includes(key)) { s1 += row } else { s2 += row } } listEl.insertAdjacentHTML('beforeend', `
    ${s1}
    ${s2}
    `) // 初始发音设置 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 += `

    ${part ? `${part}` : ''}${text} ${means ? means.join(';') : ''}

    ` } } } }) s += `
    ` } }) } if (!hasParts && word_means) s += `

    ${word_means.join(';')}

    ` // 单词形态 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 += `
    [${ph_uk}${ph_uk !== ph_us ? ' $ ' + ph_us : ''}]${ph_mp3}
    ` } // 搜狗用的牛津词典 (上一个版本,层级太深,这次改版简化了。) let wordCard = getJSONValue(r, 'textTranslate.translateData.wordCard') if (isObject(wordCard) && wordCard.usualDict) { s += `
    ` s += `
    ${text}
    ` // 查询的单词 s += phStr // 释义 let {usualDict, exchange, levelList} = wordCard if (usualDict && usualDict.length > 0) { s += `
    ` usualDict.forEach(v => { s += `

    ${v.pos ? `${v.pos}` : ''}${isArray(v.values) ? v.values.join(';') : v.values}

    ` }) s += `
    ` } // 单词形态 if (exchange) { s += `
    ` let exchangeObj = { word_third: '第三人称单数', word_pl: '复数', word_ing: '现在分词', word_past: '过去式', word_done: '过去分词', word_er: '比较级', word_est: '最高级', word_proto: '原型', } for (let [k, v] of Object.entries(exchange)) { let wordStr = '' v.forEach(word => { if (word) wordStr += `${word}` }) s += `${exchangeObj[k] || '其他'}${wordStr}` } s += `
    ` } // 单词标签 if (levelList && levelList.length > 0) { s += `
    ` levelList.forEach(tag => { if (tag) s += `${tag}` }) s += `
    ` } s += `
    ` } return {text, srcLan, tarLan, lanTTS: null, data, extra: s} }, async query(q, srcLan, tarLan) { return checkRetry(() => this.trans(q, srcLan, tarLan), 2) }, 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=1` } 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-CHS' return `https://fanyi.sogou.com/?keyword=${encodeURI(q)}&transfrom=${srcLan}&transto=${tarLan}&model=general` }, } } ================================================ FILE: src/js/translate/youdao.js ================================================ 'use strict' /** * Dream Translate * https://github.com/ryanker/dream_translate * @Author Ryan * @license MIT License */ function youdaoTranslate() { return { token: { token: '', date: 0, }, langMap: { "en": "en", "ru": "ru", "pt": "pt", "hi": "hi", "de": "de", "el": "el", "it": "it", "id": "id", "nl": "nl", "kor": "ko", "jp": "ja", "fra": "fr", "spa": "es", "ara": "ar", "dan": "da", "fin": "fi", "may": "ms", "vie": "vi", "zh": "zh-CHS" }, langMapInvert: {}, lanTTS: ["en", "zh", "jp", "kor", "fra"], md5(e) { var n = function (e, t) { return e << t | e >>> 32 - t }, r = function (e, t) { var n, r, i, a, o return i = 2147483648 & e, a = 2147483648 & t, n = 1073741824 & e, r = 1073741824 & t, o = (1073741823 & e) + (1073741823 & t), n & r ? 2147483648 ^ o ^ i ^ a : n | r ? 1073741824 & o ? 3221225472 ^ o ^ i ^ a : 1073741824 ^ o ^ i ^ a : o ^ i ^ a }, i = function (e, t, n) { return e & t | ~e & n }, a = function (e, t, n) { return e & n | t & ~n }, o = function (e, t, n) { return e ^ t ^ n }, s = function (e, t, n) { return t ^ (e | ~n) }, l = function (e, t, a, o, s, l, c) { return e = r(e, r(r(i(t, a, o), s), c)), r(n(e, l), t) }, c = function (e, t, i, o, s, l, c) { return e = r(e, r(r(a(t, i, o), s), c)), r(n(e, l), t) }, u = function (e, t, i, a, s, l, c) { return e = r(e, r(r(o(t, i, a), s), c)), r(n(e, l), t) }, d = function (e, t, i, a, o, l, c) { return e = r(e, r(r(s(t, i, a), o), c)), r(n(e, l), t) }, f = function (e) { for (var t, n = e.length, r = n + 8, i = 16 * ((r - r % 64) / 64 + 1), a = Array(i - 1), o = 0, s = 0; s < n;) { o = s % 4 * 8, a[t = (s - s % 4) / 4] = a[t] | e.charCodeAt(s) << o, s++ } return t = (s - s % 4) / 4, o = s % 4 * 8, a[t] = a[t] | 128 << o, a[i - 2] = n << 3, a[i - 1] = n >>> 29, a }, p = function (e) { var t, n = "", r = "" for (t = 0; t <= 3; t++) n += (r = "0" + (e >>> 8 * t & 255).toString(16)).substr(r.length - 2, 2) return n }, h = function (e) { e = e.replace(/\x0d\x0a/g, "\n") for (var t = "", n = 0; n < e.length; n++) { var r = e.charCodeAt(n) if (r < 128) { t += String.fromCharCode(r) } else if (r > 127 && r < 2048) { t += String.fromCharCode(r >> 6 | 192) t += String.fromCharCode(63 & r | 128) } else if (r >= 55296 && r <= 56319) { if (n + 1 < e.length) { var i = e.charCodeAt(n + 1) if (i >= 56320 && i <= 57343) { var a = 1024 * (r - 55296) + (i - 56320) + 65536 t += String.fromCharCode(240 | a >> 18 & 7), t += String.fromCharCode(128 | a >> 12 & 63), t += String.fromCharCode(128 | a >> 6 & 63), t += String.fromCharCode(128 | 63 & a), n++ } } } else { t += String.fromCharCode(r >> 12 | 224), t += String.fromCharCode(r >> 6 & 63 | 128), t += String.fromCharCode(63 & r | 128) } } return t }, m = function (e) { var t, n, i, a, o, s, m, g, v, y = Array() e = h(e), y = f(e), s = 1732584193, m = 4023233417, g = 2562383102, v = 271733878 for (t = 0; t < y.length; t += 16) { n = s, i = m, a = g, o = v, s = l(s, m, g, v, y[t + 0], 7, 3614090360), v = l(v, s, m, g, y[t + 1], 12, 3905402710), g = l(g, v, s, m, y[t + 2], 17, 606105819), m = l(m, g, v, s, y[t + 3], 22, 3250441966), s = l(s, m, g, v, y[t + 4], 7, 4118548399), v = l(v, s, m, g, y[t + 5], 12, 1200080426), g = l(g, v, s, m, y[t + 6], 17, 2821735955), m = l(m, g, v, s, y[t + 7], 22, 4249261313), s = l(s, m, g, v, y[t + 8], 7, 1770035416), v = l(v, s, m, g, y[t + 9], 12, 2336552879), g = l(g, v, s, m, y[t + 10], 17, 4294925233), m = l(m, g, v, s, y[t + 11], 22, 2304563134), s = l(s, m, g, v, y[t + 12], 7, 1804603682), v = l(v, s, m, g, y[t + 13], 12, 4254626195), g = l(g, v, s, m, y[t + 14], 17, 2792965006), m = l(m, g, v, s, y[t + 15], 22, 1236535329), s = c(s, m, g, v, y[t + 1], 5, 4129170786), v = c(v, s, m, g, y[t + 6], 9, 3225465664), g = c(g, v, s, m, y[t + 11], 14, 643717713), m = c(m, g, v, s, y[t + 0], 20, 3921069994), s = c(s, m, g, v, y[t + 5], 5, 3593408605), v = c(v, s, m, g, y[t + 10], 9, 38016083), g = c(g, v, s, m, y[t + 15], 14, 3634488961), m = c(m, g, v, s, y[t + 4], 20, 3889429448), s = c(s, m, g, v, y[t + 9], 5, 568446438), v = c(v, s, m, g, y[t + 14], 9, 3275163606), g = c(g, v, s, m, y[t + 3], 14, 4107603335), m = c(m, g, v, s, y[t + 8], 20, 1163531501), s = c(s, m, g, v, y[t + 13], 5, 2850285829), v = c(v, s, m, g, y[t + 2], 9, 4243563512), g = c(g, v, s, m, y[t + 7], 14, 1735328473), m = c(m, g, v, s, y[t + 12], 20, 2368359562), s = u(s, m, g, v, y[t + 5], 4, 4294588738), v = u(v, s, m, g, y[t + 8], 11, 2272392833), g = u(g, v, s, m, y[t + 11], 16, 1839030562), m = u(m, g, v, s, y[t + 14], 23, 4259657740), s = u(s, m, g, v, y[t + 1], 4, 2763975236), v = u(v, s, m, g, y[t + 4], 11, 1272893353), g = u(g, v, s, m, y[t + 7], 16, 4139469664), m = u(m, g, v, s, y[t + 10], 23, 3200236656), s = u(s, m, g, v, y[t + 13], 4, 681279174), v = u(v, s, m, g, y[t + 0], 11, 3936430074), g = u(g, v, s, m, y[t + 3], 16, 3572445317), m = u(m, g, v, s, y[t + 6], 23, 76029189), s = u(s, m, g, v, y[t + 9], 4, 3654602809), v = u(v, s, m, g, y[t + 12], 11, 3873151461), g = u(g, v, s, m, y[t + 15], 16, 530742520), m = u(m, g, v, s, y[t + 2], 23, 3299628645), s = d(s, m, g, v, y[t + 0], 6, 4096336452), v = d(v, s, m, g, y[t + 7], 10, 1126891415), g = d(g, v, s, m, y[t + 14], 15, 2878612391), m = d(m, g, v, s, y[t + 5], 21, 4237533241), s = d(s, m, g, v, y[t + 12], 6, 1700485571), v = d(v, s, m, g, y[t + 3], 10, 2399980690), g = d(g, v, s, m, y[t + 10], 15, 4293915773), m = d(m, g, v, s, y[t + 1], 21, 2240044497), s = d(s, m, g, v, y[t + 8], 6, 1873313359), v = d(v, s, m, g, y[t + 15], 10, 4264355552), g = d(g, v, s, m, y[t + 6], 15, 2734768916), m = d(m, g, v, s, y[t + 13], 21, 1309151649), s = d(s, m, g, v, y[t + 4], 6, 4149444226), v = d(v, s, m, g, y[t + 11], 10, 3174756917), g = d(g, v, s, m, y[t + 2], 15, 718787259), m = d(m, g, v, s, y[t + 9], 21, 3951481745), s = r(s, n), m = r(m, i), g = r(g, a), v = r(v, o) } return (p(s) + p(m) + p(g) + p(v)).toLowerCase() } return m(e) }, init() { this.langMapInvert = invertObject(this.langMap) let str = localStorage.getItem('youdaoToken') if (str) this.token = JSON.parse(str) return this }, setToken(options) { this.token = Object.assign(this.token, options) localStorage.setItem('youdaoToken', JSON.stringify(this.token)) }, getToken() { return new Promise((resolve, reject) => { httpGet('https://fanyi.youdao.com/').then(r => { let arr = r.match(/ { let tArr = r.match(/sign:n\.md5\("fanyideskweb"\+e\+i\+"([^"]+)"\)/) if (tArr) { let token = {token: tArr[1], date: Math.floor(Date.now() / 36e5)} this.setToken(token) resolve(token) } else { reject('youdao token error!') } }).catch(e => { reject('youdao js error:', e) }) } else { reject('youdao *.min.js error!') } }).catch(e => { reject('youdao home error:', e) }) }) }, addListenerRequest() { onBeforeSendHeadersAddListener(this.onChangeHeaders, {urls: ['*://fanyi.youdao.com/*'], types: ['xmlhttprequest']}) }, removeListenerRequest() { onBeforeSendHeadersRemoveListener(this.onChangeHeaders) }, onChangeHeaders(details) { let h = details.requestHeaders /*h.some((v, k) => { if (v.name.toLowerCase() === 'referer') { h.splice(k, 1) return true } })*/ h.push({name: 'Origin', value: 'https://fanyi.youdao.com'}) h.push({name: 'Referer', value: 'https://fanyi.youdao.com'}) return {requestHeaders: h} }, trans(q, srcLan, tarLan) { srcLan = this.langMap[srcLan] || 'AUTO' tarLan = this.langMap[tarLan] || 'zh-CHS' if (srcLan !== 'zh-CHS') tarLan = 'zh-CHS' // 有道只支持单一中文互换翻译 return new Promise((resolve, reject) => { if (q.length > 5000) return reject('The text is too large!') if (!this.token.token) return reject('youdao token empty!') this.addListenerRequest() let bv = this.md5(navigator.appVersion), ts = '' + (new Date).getTime(), salt = ts + Math.floor(10 * Math.random()) let sign = this.md5("fanyideskweb" + q + salt + this.token.token) let p = new URLSearchParams(`i=${q}&from=${srcLan}&to=${tarLan}&smartresult=dict&client=fanyideskweb&salt=${salt}&sign=${sign}<s=${ts}&bv=${bv}&doctype=json&version=2.1&keyfrom=fanyi.web&action=FY_BY_CLICKBUTTION`) let url = 'https://fanyi.youdao.com/translate_o?smartresult=dict&smartresult=rule' httpPost({url: url, body: p.toString()}).then(r => { this.removeListenerRequest() if (r) { resolve(this.unify(r, q, srcLan, tarLan)) } else { reject('youdao trans error!') } }).catch(e => { this.removeListenerRequest() reject(e) }) }) }, unify(r, q, srcLan, tarLan) { // console.log('youdao:', r, q, srcLan, tarLan) let lanArr = r.type.split('2') if (lanArr.length > 1) srcLan = lanArr[0] let map = this.langMapInvert srcLan = map[srcLan] || 'auto' tarLan = map[tarLan] || '' let ret = {text: q, srcLan: srcLan, tarLan: tarLan, lanTTS: this.lanTTS, data: []} let arr = r && r.translateResult arr && arr.forEach(val => { val.forEach(v => { if (v.tgt && v.src) ret.data.push({srcText: v.src, tarText: v.tgt}) }) }) 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) { return new Promise((resolve, reject) => { if (!inArray(lan, this.lanTTS)) return reject('This language is not supported!') let lanArr = {en: "eng", zh: 'zh-CHS', jp: "jap", kor: "ko", fra: "fr"} let le = lanArr[lan] || lanArr.en // resolve(`https://tts.youdao.com/fanyivoice?word=${encodeURI(q)}&le=eng&keyfrom=speaker-target`) let getUrl = (s) => { return `https://dict.youdao.com/dictvoice?audio=${encodeURIComponent(s)}&type=2` } let r = [] let arr = sliceStr(q, 128) arr.forEach(text => { r.push(getUrl(text)) }) resolve(r) }) }, link(q, srcLan, tarLan) { return `https://fanyi.youdao.com/?d_sl=${srcLan}&d_tl=${tarLan}&d_text=${encodeURI(q)}` }, } } ================================================ FILE: src/js/video.js ================================================ 'use strict' /** * Dream Translate * https://github.com/ryanker/dream_translate * @Author Ryan * @license MIT License */ let u = new URL(location.href) let video = document.createElement('video') video.setAttribute('style', 'width:400px;height:224px;outline:0') video.controls = true video.poster = u.searchParams.get('thumbUrl') video.src = u.searchParams.get('videoUrl') document.body.appendChild(video) ================================================ FILE: src/manifest.json ================================================ { "name": "梦想划词翻译—聚合词典搜索", "description": "梦想划词翻译是为阅读和学习外语而开发的一款翻译和查词工具,聚合数十款在线词典和在线翻译。", "version": "1.6.27", "manifest_version": 2, "icons": { "128": "icon/128.png" }, "background": { "scripts": [ "js/lib/md5.min.js", "js/common.js", "js/db.js", "js/background.js" ] }, "content_scripts": [ { "matches": [ "\u003Call_urls>" ], "js": [ "js/common.js", "js/content.js" ], "css": [ "css/content.css" ], "run_at": "document_start" }, { "all_frames": true, "matches": [ "\u003Call_urls>" ], "js": [ "js/frame.js" ] } ], "browser_action": { "default_popup": "html/popup.html", "default_title": "梦想划词翻译", "default_icon": "icon/128.png" }, "web_accessible_resources": [ "css/*", "html/*" ], "permissions": [ "storage", "clipboardWrite", "clipboardRead", "cookies", "contextMenus", "webRequest", "webRequestBlocking", "unlimitedStorage", "tts", "\u003Call_urls>" ], "homepage_url": "https://github.com/ryanker/dream_translate", "commands": { "_execute_browser_action": { "description": "打开翻译面板", "suggested_key": { "default": "Alt+D" } }, "openWindow": { "description": "打开翻译窗口", "global": true, "suggested_key": { "default": "Ctrl+Shift+9" } }, "toggleScribble": { "description": "划词翻译开关", "suggested_key": { "default": "Ctrl+Shift+X" } }, "cropImage": { "description": "截图识别翻译", "suggested_key": { "default": "Ctrl+Shift+A" } }, "stopPlayAudio": { "description": "停止播放声音" }, "clipboardTrans": { "description": "剪贴板内容翻译" } } } ================================================ FILE: tool/alibaba.html ================================================ Title ================================================ FILE: tool/baidu.html ================================================ Title ================================================ FILE: tool/bd.js ================================================ var bdList = { auto: { zhName: "自动检测", enName: "Auto Detect", pinyin: "zidongjiance", popularity: { zh: 201, en: 201 } }, zh: { zhName: "中文(简体)", enName: "Chinese", pinyin: "zhongwenjianti", popularity: { zh: 193, en: 30 } }, en: { zhName: "英语", enName: "English", pinyin: "yingyu", popularity: { zh: 179, en: 48 } }, jp: { zhName: "日语", enName: "Japanese", pinyin: "riyu", popularity: { zh: 132, en: 82 } }, th: { zhName: "泰语", enName: "Thai", pinyin: "taiyu", popularity: { zh: 148, en: 173 } }, spa: { zhName: "西班牙语", enName: "Spanish", pinyin: "xibanyayu", popularity: { zh: 164, en: 151 } }, ara: { zhName: "阿拉伯语", enName: "Arabic", pinyin: "alaboyu", popularity: { zh: 1, en: 1 } }, fra: { zhName: "法语", enName: "French", pinyin: "fayu", popularity: { zh: 46, en: 51 } }, kor: { zhName: "韩语", enName: "Korean", pinyin: "hanyu", popularity: { zh: 63, en: 84 } }, ru: { zhName: "俄语", enName: "Russian", pinyin: "eyu", popularity: { zh: 45, en: 146 } }, de: { zhName: "德语", enName: "German", pinyin: "deyu", popularity: { zh: 39, en: 57 } }, pt: { zhName: "葡萄牙语", enName: "Portuguese", pinyin: "putaoyayu", popularity: { zh: 125, en: 138 } }, it: { zhName: "意大利语", enName: "Italian", pinyin: "yidaliyu", popularity: { zh: 181, en: 73 } }, el: { zhName: "希腊语", enName: "Greek", pinyin: "xilayu", popularity: { zh: 166, en: 58 } }, nl: { zhName: "荷兰语", enName: "Dutch", pinyin: "helanyu", popularity: { zh: 64, en: 45 } }, pl: { zhName: "波兰语", enName: "Polish", pinyin: "bolanyu", popularity: { zh: 18, en: 137 } }, fin: { zhName: "芬兰语", enName: "Finnish", pinyin: "fenlanyu", popularity: { zh: 47, en: 52 } }, cs: { zhName: "捷克语", enName: "Czech", pinyin: "jiekeyu", popularity: { zh: 71, en: 33 } }, bul: { zhName: "保加利亚语", enName: "Bulgarian", pinyin: "baojialiyayu", popularity: { zh: 19, en: 15 } }, dan: { zhName: "丹麦语", enName: "Danish", pinyin: "danmaiyu", popularity: { zh: 40, en: 46 } }, est: { zhName: "爱沙尼亚语", enName: "Estonian", pinyin: "aishaniyayu", popularity: { zh: 2, en: 49 } }, hu: { zhName: "匈牙利语", enName: "Hungarian", pinyin: "xiongyaliyu", popularity: { zh: 165, en: 64 } }, rom: { zhName: "罗马尼亚语", enName: "Romanian", pinyin: "luomaniyayu", popularity: { zh: 91, en: 147 } }, slo: { zhName: "斯洛文尼亚语", enName: "Slovenian", pinyin: "siluowenniyayu", popularity: { zh: 134, en: 153 } }, swe: { zhName: "瑞典语", enName: "Swedish", pinyin: "ruidianyu", popularity: { zh: 133, en: 152 } }, vie: { zhName: "越南语", enName: "Vietnamese", pinyin: "yuenanyu", popularity: { zh: 180, en: 190 } }, yue: { zhName: "中文(粤语)", enName: "Cantonese", pinyin: "zhongwenyueyu", popularity: { zh: 195, en: 31 } }, cht: { zhName: "中文(繁体)", enName: "Traditional Chinese", pinyin: "zhongwenfanti", popularity: { zh: 194, en: 172 } }, wyw: { zhName: "中文(文言文)", enName: "Classical Chinese", pinyin: "zhongwenwenyanwen", popularity: { zh: 196, en: 32 } }, afr: { zhName: "南非荷兰语", enName: "Afrikaans", pinyin: "nanfeihelanyu", popularity: { zh: 121, en: 7 } }, alb: { zhName: "阿尔巴尼亚语", enName: "Albanian", pinyin: "aerbaniyayu", popularity: { zh: 4, en: 3 } }, amh: { zhName: "阿姆哈拉语", enName: "Amharic", pinyin: "amuhalayu", popularity: { zh: 6, en: 5 } }, arm: { zhName: "亚美尼亚语", enName: "Armenian", pinyin: "yameiniyayu", popularity: { zh: 184, en: 4 } }, asm: { zhName: "阿萨姆语", enName: "Assamese", pinyin: "asamuyu", popularity: { zh: 7, en: 6 } }, ast: { zhName: "阿斯图里亚斯语", enName: "Asturian", pinyin: "asituliyasiyu", popularity: { zh: 12, en: 13 } }, aze: { zhName: "阿塞拜疆语", enName: "Azerbaijani", pinyin: "asaibaijiangyu", popularity: { zh: 3, en: 2 } }, baq: { zhName: "巴斯克语", enName: "Basque", pinyin: "basikeyu", popularity: { zh: 23, en: 20 } }, bel: { zhName: "白俄罗斯语", enName: "Belarusian", pinyin: "baieluosiyu", popularity: { zh: 21, en: 18 } }, ben: { zhName: "孟加拉语", enName: "Bengali", pinyin: "mengjialayu", popularity: { zh: 108, en: 17 } }, bos: { zhName: "波斯尼亚语", enName: "Bosnian", pinyin: "bosiniyayu", popularity: { zh: 22, en: 19 } }, bur: { zhName: "缅甸语", enName: "Burmese", pinyin: "miandianyu", popularity: { zh: 106, en: 16 } }, cat: { zhName: "加泰罗尼亚语", enName: "Catalan", pinyin: "jiatailuoniyayu", popularity: { zh: 72, en: 34 } }, ceb: { zhName: "宿务语", enName: "Cebuano", pinyin: "suwuyu", popularity: { zh: 168, en: 36 } }, hrv: { zhName: "克罗地亚语", enName: "Croatian", pinyin: "keluodiyayu", popularity: { zh: 76, en: 35 } }, epo: { zhName: "世界语", enName: "Esperanto", pinyin: "shijieyu", popularity: { zh: 141, en: 50 } }, fao: { zhName: "法罗语", enName: "Faroese", pinyin: "faluoyu", popularity: { zh: 50, en: 55 } }, fil: { zhName: "菲律宾语", enName: "Filipino", pinyin: "feilvbinyu", popularity: { zh: 48, en: 53 } }, glg: { zhName: "加利西亚语", enName: "Galician", pinyin: "jialixiyayu", popularity: { zh: 73, en: 60 } }, geo: { zhName: "格鲁吉亚语", enName: "Georgian", pinyin: "gelujiyayu", popularity: { zh: 54, en: 59 } }, guj: { zhName: "古吉拉特语", enName: "Gujarati", pinyin: "gujilateyu", popularity: { zh: 55, en: 61 } }, hau: { zhName: "豪萨语", enName: "Hausa", pinyin: "haosayu", popularity: { zh: 67, en: 68 } }, heb: { zhName: "希伯来语", enName: "Hebrew", pinyin: "xibolaiyu", popularity: { zh: 167, en: 65 } }, hi: { zhName: "印地语", enName: "Hindi", pinyin: "yindiyu", popularity: { zh: 183, en: 66 } }, ice: { zhName: "冰岛语", enName: "Icelandic", pinyin: "bingdaoyu", popularity: { zh: 24, en: 76 } }, ibo: { zhName: "伊博语", enName: "Igbo", pinyin: "yiboyu", popularity: { zh: 186, en: 75 } }, id: { zhName: "印尼语", enName: "Indonesian", pinyin: "yinniyu", popularity: { zh: 182, en: 74 } }, gle: { zhName: "爱尔兰语", enName: "Irish", pinyin: "aierlanyu", popularity: { zh: 5, en: 77 } }, kan: { zhName: "卡纳达语", enName: "Kannada", pinyin: "kanadayu", popularity: { zh: 77, en: 87 } }, kaz: { zhName: "哈萨克语", enName: "Kazakh", pinyin: "hasakeyu", popularity: { zh: 65, en: 85 } }, kli: { zhName: "克林贡语", enName: "Klingon", pinyin: "kelingongyu", popularity: { zh: 89, en: 98 } }, kur: { zhName: "库尔德语", enName: "Kurdish", pinyin: "kuerdeyu", popularity: { zh: 80, en: 88 } }, lao: { zhName: "老挝语", enName: "Lao", pinyin: "laozhuayu", popularity: { zh: 92, en: 99 } }, lat: { zhName: "拉丁语", enName: "Latin", pinyin: "ladingyu", popularity: { zh: 93, en: 100 } }, lav: { zhName: "拉脱维亚语", enName: "Latvian", pinyin: "latuoweiyayu", popularity: { zh: 95, en: 102 } }, lit: { zhName: "立陶宛语", enName: "Lithuanian", pinyin: "litaowanyu", popularity: { zh: 94, en: 101 } }, ltz: { zhName: "卢森堡语", enName: "Luxembourgish", pinyin: "lusenbaoyu", popularity: { zh: 96, en: 103 } }, mac: { zhName: "马其顿语", enName: "Macedonian", pinyin: "maqidunyu", popularity: { zh: 109, en: 112 } }, mg: { zhName: "马拉加斯语", enName: "Malagasy", pinyin: "malajiasiyu", popularity: { zh: 114, en: 118 } }, may: { zhName: "马来语", enName: "Malay", pinyin: "malaiyu", popularity: { zh: 107, en: 111 } }, mal: { zhName: "马拉雅拉姆语", enName: "Malayalam", pinyin: "malayalamuyu", popularity: { zh: 111, en: 115 } }, mlt: { zhName: "马耳他语", enName: "Maltese", pinyin: "maertayu", popularity: { zh: 112, en: 116 } }, mar: { zhName: "马拉地语", enName: "Marathi", pinyin: "maladiyu", popularity: { zh: 110, en: 114 } }, nep: { zhName: "尼泊尔语", enName: "Nepali", pinyin: "niboeryu", popularity: { zh: 120, en: 125 } }, nno: { zhName: "新挪威语", enName: "Nynorsk", pinyin: "xinnuoweiyu", popularity: { zh: 178, en: 130 } }, per: { zhName: "波斯语", enName: "Persian", pinyin: "bosiyu", popularity: { zh: 20, en: 139 } }, srd: { zhName: "萨丁尼亚语", enName: "Sardinian", pinyin: "sadingniyayu", popularity: { zh: 143, en: 167 } }, srp: { zhName: "塞尔维亚语(拉丁文)", enName: "Serbian", pinyin: "saierweiyayu", popularity: { zh: 137, en: 156 } }, sin: { zhName: "僧伽罗语 ", enName: "Sinhala", pinyin: "sengqieluoyu", popularity: { zh: 136, en: 155 } }, sk: { zhName: "斯洛伐克语", enName: "Slovak", pinyin: "siluofakeyu", popularity: { zh: 135, en: 154 } }, som: { zhName: "索马里语", enName: "Somali", pinyin: "suomaliyu", popularity: { zh: 139, en: 159 } }, swa: { zhName: "斯瓦希里语", enName: "Swahili", pinyin: "siwaxiliyu", popularity: { zh: 138, en: 158 } }, tgl: { zhName: "他加禄语", enName: "Tagalog", pinyin: "tajialuyu", popularity: { zh: 154, en: 181 } }, tgk: { zhName: "塔吉克语", enName: "Tajik", pinyin: "tajikeyu", popularity: { zh: 152, en: 176 } }, tam: { zhName: "泰米尔语", enName: "Tamil", pinyin: "taimieryu", popularity: { zh: 150, en: 175 } }, tat: { zhName: "鞑靼语", enName: "Tatar", pinyin: "dadayu", popularity: { zh: 43, en: 182 } }, tel: { zhName: "泰卢固语", enName: "Telugu", pinyin: "tailuguyu", popularity: { zh: 153, en: 179 } }, tr: { zhName: "土耳其语", enName: "Turkish", pinyin: "tuerqiyu", popularity: { zh: 149, en: 174 } }, tuk: { zhName: "土库曼语", enName: "Turkmen", pinyin: "tukumanyu", popularity: { zh: 151, en: 177 } }, ukr: { zhName: "乌克兰语", enName: "Ukrainian", pinyin: "wukelanyu", popularity: { zh: 157, en: 186 } }, urd: { zhName: "乌尔都语", enName: "Urdu", pinyin: "wuerduyu", popularity: { zh: 158, en: 187 } }, uzb: { zhName: "乌兹别克语", enName: "Uzbek", pinyin: "wuzibiekeyu", popularity: { zh: 159, en: 188 } }, oci: { zhName: "奥克语", enName: "Occitan", pinyin: "aokeyu", popularity: { zh: 15, en: 132 } }, kir: { zhName: "吉尔吉斯语", enName: "Kyrgyz", pinyin: "jierjisiyu", popularity: { zh: 74, en: 93 } }, pus: { zhName: "普什图语", enName: "Pashto", pinyin: "pushituyu", popularity: { zh: 126, en: 140 } }, hkm: { zhName: "高棉语", enName: "Khmer", pinyin: "gaomianyu", popularity: { zh: 53, en: 86 } }, ht: { zhName: "海地语", enName: "Haitian Creole", pinyin: "haidiyu", popularity: { zh: 68, en: 69 } }, nob: { zhName: "书面挪威语", enName: "Bokmål", pinyin: "shumiannuoweiyu", popularity: { zh: 147, en: 28 } }, pan: { zhName: "旁遮普语", enName: "Punjabi", pinyin: "pangzhepuyu", popularity: { zh: 127, en: 141 } }, arq: { zhName: "阿尔及利亚阿拉伯语", enName: "Algerian Arabic", pinyin: "aerjiliyaalaboyu", popularity: { zh: 9, en: 10 } }, bis: { zhName: "比斯拉马语", enName: "Bislama", pinyin: "bisilamayu", popularity: { zh: 26, en: 21 } }, frn: { zhName: "加拿大法语", enName: "Canadian French", pinyin: "jianadafayu", popularity: { zh: 75, en: 39 } }, hak: { zhName: "哈卡钦语", enName: "Hakha Chin", pinyin: "hakaqinyu", popularity: { zh: 69, en: 70 } }, hup: { zhName: "胡帕语", enName: "Hupa", pinyin: "hupayu", popularity: { zh: 70, en: 72 } }, ing: { zhName: "印古什语", enName: "Ingush", pinyin: "yingushiyu", popularity: { zh: 192, en: 79 } }, lag: { zhName: "拉特加莱语", enName: "Latgalian", pinyin: "latejialaiyu", popularity: { zh: 99, en: 105 } }, mau: { zhName: "毛里求斯克里奥尔语", enName: "Mauritian Creole", pinyin: "maoliqiusikeliaoeryu", popularity: { zh: 118, en: 122 } }, mot: { zhName: "黑山语", enName: "Montenegrin", pinyin: "heishanyu", popularity: { zh: 66, en: 113 } }, pot: { zhName: "巴西葡萄牙语", enName: "Brazilian Portuguese", pinyin: "baxiputaoyayu", popularity: { zh: 28, en: 144 } }, ruy: { zhName: "卢森尼亚语", enName: "Rusyn", pinyin: "lusenniyayu", popularity: { zh: 102, en: 150 } }, sec: { zhName: "塞尔维亚-克罗地亚语", enName: "Serbo-Croatian", pinyin: "saierweiya-keluodiyayu", popularity: { zh: 144, en: 168 } }, sil: { zhName: "西里西亚语", enName: "Silesian", pinyin: "xilixiyayu", popularity: { zh: 175, en: 170 } }, tua: { zhName: "突尼斯阿拉伯语", enName: "Tunisian Arabic", pinyin: "tunisialaboyu", popularity: { zh: 156, en: 184 } }, ach: { zhName: "亚齐语", enName: "Achinese", pinyin: "yaqiyu", popularity: { zh: 188, en: 8 } }, aka: { zhName: "阿肯语", enName: "Akan", pinyin: "akenyu", popularity: { zh: 10, en: 9 } }, arg: { zhName: "阿拉贡语", enName: "Aragonese", pinyin: "alagongyu", popularity: { zh: 11, en: 12 } }, aym: { zhName: "艾马拉语", enName: "Aymara", pinyin: "aimalayu", popularity: { zh: 13, en: 14 } }, bal: { zhName: "俾路支语", enName: "Baluchi", pinyin: "biluzhiyu", popularity: { zh: 34, en: 22 } }, bak: { zhName: "巴什基尔语", enName: "Bashkir", pinyin: "bashijieryu", popularity: { zh: 27, en: 23 } }, bem: { zhName: "本巴语", enName: "Bemba", pinyin: "benbayu", popularity: { zh: 32, en: 24 } }, ber: { zhName: "柏柏尔语", enName: "Berber languages", pinyin: "baibaieryu", popularity: { zh: 29, en: 25 } }, bho: { zhName: "博杰普尔语", enName: "Bhojpuri", pinyin: "bojiepueryu", popularity: { zh: 35, en: 26 } }, bli: { zhName: "比林语", enName: "Blin", pinyin: "bilinyu", popularity: { zh: 33, en: 27 } }, bre: { zhName: "布列塔尼语", enName: "Breton", pinyin: "bulietaniyu", popularity: { zh: 36, en: 29 } }, chr: { zhName: "切罗基语", enName: "Cherokee", pinyin: "qieluojiyu", popularity: { zh: 131, en: 40 } }, nya: { zhName: "齐切瓦语", enName: "Chichewa", pinyin: "qiqiewayu", popularity: { zh: 129, en: 38 } }, chv: { zhName: "楚瓦什语", enName: "Chuvash", pinyin: "chuwashiyu", popularity: { zh: 38, en: 41 } }, cor: { zhName: "康瓦尔语", enName: "Cornish", pinyin: "kangwaeryu", popularity: { zh: 86, en: 42 } }, cos: { zhName: "科西嘉语", enName: "Corsican", pinyin: "kexijiayu", popularity: { zh: 79, en: 37 } }, cre: { zhName: "克里克语", enName: "Creek", pinyin: "kelikeyu", popularity: { zh: 87, en: 43 } }, cri: { zhName: "克里米亚鞑靼语", enName: "Crimean Tatar", pinyin: "kelimiyadadayu", popularity: { zh: 88, en: 44 } }, div: { zhName: "迪维希语", enName: "Divehi", pinyin: "diweixiyu", popularity: { zh: 41, en: 47 } }, eno: { zhName: "古英语", enName: "Old English", pinyin: "guyingyu", popularity: { zh: 62, en: 134 } }, frm: { zhName: "中古法语", enName: "Middle French", pinyin: "zhonggufayu", popularity: { zh: 200, en: 123 } }, fri: { zhName: "弗留利语", enName: "Friulian", pinyin: "fuliuliyu", popularity: { zh: 52, en: 56 } }, ful: { zhName: "富拉尼语", enName: "Fulani", pinyin: "fulaniyu", popularity: { zh: 49, en: 54 } }, gla: { zhName: "盖尔语", enName: "Gaelic", pinyin: "gaieryu", popularity: { zh: 59, en: 63 } }, lug: { zhName: "卢干达语", enName: "Luganda", pinyin: "lugandayu", popularity: { zh: 101, en: 110 } }, gra: { zhName: "古希腊语", enName: "Ancient Greek", pinyin: "guxilayu", popularity: { zh: 61, en: 11 } }, grn: { zhName: "瓜拉尼语", enName: "Guarani", pinyin: "gualaniyu", popularity: { zh: 57, en: 62 } }, haw: { zhName: "夏威夷语", enName: "Hawaiian", pinyin: "xiaweiyiyu", popularity: { zh: 171, en: 67 } }, hil: { zhName: "希利盖农语", enName: "Hiligaynon", pinyin: "xiligainongyu", popularity: { zh: 176, en: 71 } }, ido: { zhName: "伊多语", enName: "Ido", pinyin: "yiduoyu", popularity: { zh: 189, en: 78 } }, ina: { zhName: "因特语", enName: "Interlingua ", pinyin: "yinteyu", popularity: { zh: 191, en: 80 } }, iku: { zhName: "伊努克提图特语", enName: "Inuktitut", pinyin: "yinuketituteyu", popularity: { zh: 190, en: 81 } }, jav: { zhName: "爪哇语", enName: "Javanese", pinyin: "zhaowayu", popularity: { zh: 198, en: 83 } }, kab: { zhName: "卡拜尔语", enName: "Kabyle", pinyin: "kabaieryu", popularity: { zh: 83, en: 94 } }, kal: { zhName: "格陵兰语", enName: "Kalaallisut", pinyin: "gelinglanyu", popularity: { zh: 58, en: 92 } }, kau: { zhName: "卡努里语", enName: "Kanuri", pinyin: "kanuliyu", popularity: { zh: 84, en: 95 } }, kas: { zhName: "克什米尔语", enName: "Kashmiri", pinyin: "keshimieryu", popularity: { zh: 82, en: 90 } }, kah: { zhName: "卡舒比语", enName: "Kashubian", pinyin: "kashubiyu", popularity: { zh: 85, en: 96 } }, kin: { zhName: "卢旺达语", enName: "Kinyarwanda", pinyin: "luwangdayu", popularity: { zh: 103, en: 97 } }, kon: { zhName: "刚果语", enName: "Kongo", pinyin: "gangguoyu", popularity: { zh: 56, en: 91 } }, kok: { zhName: "孔卡尼语", enName: "Konkani", pinyin: "kongkaniyu", popularity: { zh: 81, en: 89 } }, lim: { zhName: "林堡语", enName: "Limburgish", pinyin: "linbaoyu", popularity: { zh: 100, en: 106 } }, lin: { zhName: "林加拉语", enName: "Lingala", pinyin: "linjialayu", popularity: { zh: 97, en: 104 } }, loj: { zhName: "逻辑语", enName: "Lojban", pinyin: "luojiyu", popularity: { zh: 105, en: 107 } }, log: { zhName: "低地德语", enName: "Low German", pinyin: "didideyu", popularity: { zh: 44, en: 108 } }, los: { zhName: "下索布语", enName: "Lower Sorbian", pinyin: "xiasuobuyu", popularity: { zh: 177, en: 109 } }, mai: { zhName: "迈蒂利语", enName: "Maithili", pinyin: "maidiliyu", popularity: { zh: 115, en: 119 } }, glv: { zhName: "曼克斯语", enName: "Manx", pinyin: "mankesiyu", popularity: { zh: 117, en: 121 } }, mao: { zhName: "毛利语", enName: "Maori", pinyin: "maoliyu", popularity: { zh: 113, en: 117 } }, mah: { zhName: "马绍尔语", enName: "Marshallese", pinyin: "mashaoeryu", popularity: { zh: 116, en: 120 } }, nbl: { zhName: "南恩德贝莱语", enName: "Southern Ndebele", pinyin: "nanendebeilaiyu", popularity: { zh: 123, en: 163 } }, nea: { zhName: "那不勒斯语", enName: "Neapolitan", pinyin: "nabulesiyu", popularity: { zh: 124, en: 127 } }, nqo: { zhName: "西非书面语", enName: "N'Ko", pinyin: "xifeishumianyu", popularity: { zh: 173, en: 128 } }, sme: { zhName: "北方萨米语", enName: "Northern Sami", pinyin: "beifangsamiyu", popularity: { zh: 31, en: 129 } }, nor: { zhName: "挪威语", enName: "Norwegian", pinyin: "nuoweiyu", popularity: { zh: 119, en: 124 } }, oji: { zhName: "奥杰布瓦语", enName: "Ojibwa", pinyin: "aojiebuwayu", popularity: { zh: 14, en: 133 } }, ori: { zhName: "奥里亚语", enName: "Oriya", pinyin: "aoliyayu", popularity: { zh: 8, en: 131 } }, orm: { zhName: "奥罗莫语", enName: "Oromo", pinyin: "aoluomoyu", popularity: { zh: 16, en: 135 } }, oss: { zhName: "奥塞梯语", enName: "Ossetian", pinyin: "aosaitiyu", popularity: { zh: 17, en: 136 } }, pam: { zhName: "邦板牙语", enName: "Pampanga", pinyin: "bangbanyayu", popularity: { zh: 30, en: 143 } }, pap: { zhName: "帕皮阿门托语", enName: "Papiamento", pinyin: "papiamentuoyu", popularity: { zh: 128, en: 142 } }, ped: { zhName: "北索托语", enName: "Northern Sotho", pinyin: "beisuotuoyu", popularity: { zh: 25, en: 126 } }, que: { zhName: "克丘亚语", enName: "Quechua", pinyin: "keqiuyayu", popularity: { zh: 90, en: 145 } }, roh: { zhName: "罗曼什语", enName: "Romansh", pinyin: "luomanshiyu", popularity: { zh: 98, en: 148 } }, ro: { zhName: "罗姆语", enName: "Romany", pinyin: "luomuyu", popularity: { zh: 104, en: 149 } }, sm: { zhName: "萨摩亚语", enName: "Samoan", pinyin: "samoyayu", popularity: { zh: 140, en: 161 } }, san: { zhName: "梵语", enName: "Sanskrit", pinyin: "fanyu", popularity: { zh: 51, en: 164 } }, sco: { zhName: "苏格兰语", enName: "Scots", pinyin: "sugelanyu", popularity: { zh: 142, en: 165 } }, sha: { zhName: "掸语", enName: "Shan", pinyin: "shanyu", popularity: { zh: 145, en: 169 } }, sna: { zhName: "修纳语", enName: "Shona", pinyin: "xiunayu", popularity: { zh: 170, en: 160 } }, snd: { zhName: "信德语", enName: "Sindhi", pinyin: "xindeyu", popularity: { zh: 169, en: 157 } }, sol: { zhName: "桑海语", enName: "Songhai languages", pinyin: "sanghaiyu", popularity: { zh: 146, en: 171 } }, sot: { zhName: "南索托语", enName: "Southern Sotho", pinyin: "nansuotuoyu", popularity: { zh: 122, en: 162 } }, syr: { zhName: "叙利亚语", enName: "Syriac", pinyin: "xuliyayu", popularity: { zh: 172, en: 166 } }, tet: { zhName: "德顿语", enName: "Tetum", pinyin: "dedunyu", popularity: { zh: 42, en: 178 } }, tir: { zhName: "提格利尼亚语", enName: "Tigrinya", pinyin: "tigeliniyayu", popularity: { zh: 155, en: 183 } }, tso: { zhName: "聪加语", enName: "Tsonga", pinyin: "congjiayu", popularity: { zh: 37, en: 180 } }, twi: { zhName: "契维语", enName: "Twi", pinyin: "qiweiyu", popularity: { zh: 130, en: 185 } }, ups: { zhName: "高地索布语", enName: "Upper Sorbian", pinyin: "gaodisuobuyu", popularity: { zh: 60, en: 189 } }, ven: { zhName: "文达语", enName: "Venda", pinyin: "wendayu", popularity: { zh: 162, en: 191 } }, wln: { zhName: "瓦隆语", enName: "Walloon", pinyin: "walongyu", popularity: { zh: 163, en: 194 } }, wel: { zhName: "威尔士语", enName: "Welsh", pinyin: "weiershiyu", popularity: { zh: 160, en: 192 } }, fry: { zhName: "西弗里斯语", enName: "Western Frisian", pinyin: "xifulisiyu", popularity: { zh: 174, en: 195 } }, wol: { zhName: "沃洛夫语", enName: "Wolof", pinyin: "woluofuyu", popularity: { zh: 161, en: 193 } }, xho: { zhName: "科萨语", enName: "Xhosa", pinyin: "kesayu", popularity: { zh: 78, en: 196 } }, yid: { zhName: "意第绪语", enName: "Yiddish", pinyin: "yidixuyu", popularity: { zh: 187, en: 198 } }, yor: { zhName: "约鲁巴语", enName: "Yoruba", pinyin: "yuelubayu", popularity: { zh: 185, en: 197 } }, zaz: { zhName: "扎扎其语", enName: "Zaza", pinyin: "zhazhaqiyu", popularity: { zh: 199, en: 200 } }, zul: { zhName: "祖鲁语", enName: "Zulu", pinyin: "zuluyu", popularity: { zh: 197, en: 199 } }, sun: { zhName: "巽他语", enName: "BasaSunda", pinyin: "xuntayu", popularity: { zh: 172.5, en: 29.5 } }, hmn: { zhName: "苗语", enName: "Hmong", pinyin: "miaoyu", popularity: { zh: 107.5, en: 72.5 } }, src: { zhName: "塞尔维亚语(西里尔文)", enName: "Serb(Cyrillic)", pinyin: "saierweiyayu", popularity: { zh: 137.5, en: 156.5 } } } ================================================ FILE: tool/bing.html ================================================ Title
    ================================================ 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
    葡萄牙语(葡萄牙)Portuguese (Portugal) pt-PT 语言模型Language model
    罗马尼亚语(罗马尼亚)Romanian (Romania) ro-RO 语言模型Language model
    俄语(俄罗斯)Russian (Russia) ru-RU 声学模型Acoustic model
    语言模型Language model
    斯洛伐克语(斯洛伐克)Slovak (Slovakia) sk-SK 语言模型Language model
    斯洛文尼亚语(斯洛文尼亚)Slovenian (Slovenia) sl-SI 语言模型Language model
    西班牙语(阿根廷)Spanish (Argentina) es-AR 语言模型Language Model
    西班牙语(玻利维亚)Spanish (Bolivia) es-BO 语言模型Language Model
    西班牙语(智利)Spanish (Chile) es-CL 语言模型Language Model
    西班牙语(哥伦比亚)Spanish (Colombia) es-CO 语言模型Language Model
    西班牙语(哥斯达黎加)Spanish (Costa Rica) es-CR 语言模型Language Model
    西班牙语(古巴)Spanish (Cuba) es-CU 语言模型Language Model
    西班牙语(多米尼加共和国)Spanish (Dominican Republic) es-DO 语言模型Language Model
    西班牙语(厄瓜多尔)Spanish (Ecuador) es-EC 语言模型Language Model
    西班牙语(萨尔瓦多)Spanish (El Salvador) es-SV 语言模型Language Model
    西班牙语(危地马拉)Spanish (Guatemala) es-GT 语言模型Language Model
    西班牙语(洪都拉斯)Spanish (Honduras) es-HN 语言模型Language Model
    西班牙语(墨西哥)Spanish (Mexico) es-MX 声学模型Acoustic model
    语言模型Language model
    西班牙(尼加拉瓜)Spanish (Nicaragua) es-NI 语言模型Language Model
    西班牙语(巴拿马)Spanish (Panama) es-PA 语言模型Language Model
    西班牙语(巴拉圭)Spanish (Paraguay) es-PY 语言模型Language Model
    西班牙语(秘鲁)Spanish (Peru) es-PE 语言模型Language Model
    西班牙语(波多黎各)Spanish (Puerto Rico) es-PR 语言模型Language Model
    西班牙语(西班牙)Spanish (Spain) es-ES 声学模型Acoustic model
    语言模型Language model
    西班牙语(乌拉圭)Spanish (Uruguay) es-UY 语言模型Language Model
    西班牙语(美国)Spanish (USA) es-US 语言模型Language Model
    西班牙语(委内瑞拉)Spanish (Venezuela) es-VE 语言模型Language Model
    瑞典语(瑞典)Swedish (Sweden) sv-SE 语言模型Language model
    泰米尔语(印度)Tamil (India) ta-IN 语言模型Language model
    泰卢固语(印度)Telugu (India) te-IN 语言模型Language model
    泰语(泰国)Thai (Thailand) th-TH 语言模型Language model
    土耳其语(土耳其)Turkish (Turkey) tr-TR 语言模型Language model
    神经语音
    语言Language LocaleLocale 性别Gender 语音名称Voice name 风格支持Style support
    阿拉伯语(埃及)Arabic (Egypt) ar-EG FemaleFemale ar-EG-SalmaNeural 常规General
    阿拉伯语(沙特阿拉伯)Arabic (Saudi Arabia) ar-SA Female ar-SA-ZariyahNeural 常规General
    保加利亚语(保加利亚)Bulgarian (Bulgary) bg-BG FemaleFemale bg-BG-KalinaNeural 新建bg-BG-KalinaNeural New 常规General
    加泰罗尼亚语(西班牙)Catalan (Spain) ca-ES Female ca-ES-AlbaNeural 常规General
    中文(粤语,繁体)Chinese (Cantonese, Traditional) zh-HK FemaleFemale zh-HK-HiuGaaiNeural 常规General
    中文(普通话,简体)Chinese (Mandarin, Simplified) zh-CN FemaleFemale zh-CN-XiaoxiaoNeural 常规,使用 SSML 可以使用多种语音样式General, multiple voice styles available using SSML
    中文(普通话,简体)Chinese (Mandarin, Simplified) zh-CN Female zh-CN-XiaoyouNeural 儿童语音,针对讲故事进行了优化Kid voice, optimized for story narrating
    中文(普通话,简体)Chinese (Mandarin, Simplified) zh-CN Male zh-CN-YunyangNeural 针对新闻阅读进行了优化,Optimized for news reading,
    使用 SSML提供多种语音样式multiple voice styles available using SSML
    中文(普通话,简体)Chinese (Mandarin, Simplified) zh-CN Male zh-CN-YunyeNeural 针对讲故事进行了优化Optimized for story narrating
    中文(台湾普通话)Chinese (Taiwanese Mandarin) zh-TW FemaleFemale zh-TW-HsiaoYuNeural 常规General
    克罗地亚语(克罗地亚)Croatian (Croatia) hr-HR FemaleFemale hr-HR-GabrijelaNeural 新建hr-HR-GabrijelaNeural New 常规General
    捷克语(捷克)Czech (Czech) cs-CZ FemaleFemale cs-CZ-VlastaNeural 新建cs-CZ-VlastaNeural New 常规General
    丹麦语(丹麦)Danish (Denmark) da-DK Female da-DK-ChristelNeural 常规General
    荷兰语(荷兰)Dutch (Netherlands) nl-NL Female nl-NL-ColetteNeural 常规General
    英语(澳大利亚)English (Australia) en-AU FemaleFemale en-AU-NatashaNeural 常规General
    英语(澳大利亚)English (Australia) en-AU Male en-AU-WilliamNeural 新建en-AU-WilliamNeural New 常规General
    英语(加拿大)English (Canada) en-CA FemaleFemale en-CA-ClaraNeural 常规General
    英语(印度)English (India) en-IN FemaleFemale en-IN-NeerjaNeural 常规General
    英语(爱尔兰)English (Ireland) en-IE FemaleFemale en-IE-EmilyNeural 新建en-IE-EmilyNeural New 常规General
    英语(英国)English (United Kingdom) en-GB Female en-GB-LibbyNeural 常规General
    英语(英国)English (United Kingdom) en-GB Female en-GB-MiaNeural 常规General
    英语(英国)English (United Kingdom) en-GB Male en-GB-RyanNeural 新建en-GB-RyanNeural New 常规General
    英语(美国)English (United States) en-US FemaleFemale en-US-AriaNeural 常规,使用 SSML 可以使用多种语音样式General, multiple voice styles available using SSML
    英语(美国)English (United States) en-US Male en-US-GuyNeural 常规General
    英语(美国)English (United States) en-US FemaleFemale en-US-JennyNeural 新建en-US-JennyNeural New 常规,使用 SSML 可以使用多种语音样式General, multiple voice styles available using SSML
    芬兰语(芬兰)Finnish (Finland) fi-FI Female fi-FI-NooraNeural 常规General
    法语(加拿大)French (Canada) fr-CA FemaleFemale fr-CA-SylvieNeural 常规General
    法语(加拿大)French (Canada) fr-CA Male fr-CA-JeanNeural 新建fr-CA-JeanNeural New 常规General
    法语(法国)French (France) fr-FR FemaleFemale fr-FR-DeniseNeural 常规General
    法语(法国)French (France) fr-FR Male fr-FR-HenriNeural 新建fr-FR-HenriNeural New 常规General
    法语(瑞士)French (Switzerland) fr-CH FemaleFemale fr-CH-ArianeNeural 新建fr-CH-ArianeNeural New 常规General
    德语(奥地利)German (Austria) de-AT FemaleFemale de-AT-IngridNeural 新建de-AT-IngridNeural New 常规General
    德语(德国)German (Germany) de-DE FemaleFemale de-DE-KatjaNeural 常规General
    德语(德国)German (Germany) de-DE Male de-DE-ConradNeural 新建de-DE-ConradNeural New 常规General
    德语(瑞士)German (Switzerland) de-CH FemaleFemale de-CH-LeniNeural 新建de-CH-LeniNeural New 常规General
    希腊语(希腊)Greek (Greece) el-GR FemaleFemale el-GR-AthinaNeural 新建el-GR-AthinaNeural New 常规General
    希伯来语(以色列)Hebrew (Israel) he-IL FemaleFemale he-IL-HilaNeural 新建he-IL-HilaNeural New 常规General
    印地语(印度)Hindi (India) hi-IN FemaleFemale hi-IN-SwaraNeural 常规General
    匈牙利语(匈牙利)Hungarian (Hungary) hu-HU FemaleFemale hu-HU-NoemiNeural 新建hu-HU-NoemiNeural New 常规General
    印度尼西亚语(印度尼西亚)Indonesian (Indonesia) id-ID Male id-ID-ArdiNeural 新建id-ID-ArdiNeural New 常规General
    意大利语(意大利)Italian (Italy) it-IT FemaleFemale it-IT-ElsaNeural 常规General
    意大利语(意大利)Italian (Italy) it-IT FemaleFemale it-IT-IsabellaNeural 新建it-IT-IsabellaNeural New 常规General
    意大利语(意大利)Italian (Italy) it-IT Male it-IT-DiegoNeural 新建it-IT-DiegoNeural New 常规General
    日语(日本)Japanese (Japan) ja-JP Female ja-JP-NanamiNeural 常规General
    日语(日本)Japanese (Japan) ja-JP Male ja-JP-KeitaNeural 新建ja-JP-KeitaNeural New 常规General
    韩语(韩国)Korean (Korea) ko-KR Female ko-KR-SunHiNeural 常规General
    韩语(韩国)Korean (Korea) ko-KR Male ko-KR-InJoonNeural 新建ko-KR-InJoonNeural New 常规General
    马来语(马来西亚)Malay (Malaysia) ms-MY FemaleFemale ms-MY-YasminNeural 新建ms-MY-YasminNeural New 常规General
    挪威语(博克马尔语,挪威)Norwegian (Bokmål, Norway) nb-NO Female nb-NO-IselinNeural 常规General
    波兰语(波兰)Polish (Poland) pl-PL Female pl-PL-ZofiaNeural 常规General
    葡萄牙语(巴西)Portuguese (Brazil) pt-BR FemaleFemale pt-BR-FranciscaNeural 常规,使用 SSML 可以使用多种语音样式General, multiple voice styles available using SSML
    葡萄牙语(巴西)Portuguese (Brazil) pt-BR Male pt-BR-AntonioNeural 新建pt-BR-AntonioNeural New 常规General
    葡萄牙语(葡萄牙)Portuguese (Portugal) pt-PT FemaleFemale pt-PT-FernandaNeural 常规General
    罗马尼亚语(罗马尼亚)Romanian (Romania) ro-RO FemaleFemale ro-RO-AlinaNeural 新建ro-RO-AlinaNeural New 常规General
    俄语(俄罗斯)Russian (Russia) ru-RU Female ru-RU-DariyaNeural 常规General
    斯洛伐克语(斯洛伐克)Slovak (Slovakia) sk-SK FemaleFemale sk-SK-ViktoriaNeural 新建sk-SK-ViktoriaNeural New 常规General
    斯洛文尼亚语(斯洛文尼亚)Slovenian (Slovenia) sl-SI FemaleFemale sl-SI-PetraNeural 新建sl-SI-PetraNeural New 常规General
    西班牙语(墨西哥)Spanish (Mexico) es-MX FemaleFemale es-MX-DaliaNeural 常规General
    西班牙语(墨西哥)Spanish (Mexico) es-MX Male es-MX-JorgeNeural 新建es-MX-JorgeNeural New 常规General
    西班牙语(西班牙)Spanish (Spain) es-ES FemaleFemale es-ES-ElviraNeural 常规General
    西班牙语(西班牙)Spanish (Spain) es-ES Male es-ES-AlvaroNeural 新建es-ES-AlvaroNeural New 常规General
    瑞典语(瑞典)Swedish (Sweden) sv-SE Female sv-SE-HilleviNeural 常规General
    泰米尔语(印度)Tamil (India) ta-IN FemaleFemale ta-IN-PallaviNeural 新建ta-IN-PallaviNeural New 常规General
    泰卢固语(印度)Telugu (India) te-IN FemaleFemale te-IN-ShrutiNeural 新建te-IN-ShrutiNeural New 常规General
    泰语(泰国)Thai (Thailand) th-TH Female th-TH-AcharaNeural 常规General
    泰语(泰国)Thai (Thailand) th-TH FemaleFemale th-TH-PremwadeeNeural 新建th-TH-PremwadeeNeural New 常规General
    土耳其语(土耳其)Turkish (Turkey) tr-TR FemaleFemale tr-TR-EmelNeural 常规General
    越南语(越南)Vietnamese (Vietnam) vi-VN 新建vi-VN New Female vi-VN-HoaiMyNeural 常规General
    ================================================ FILE: tool/qq.html ================================================ Title ================================================ FILE: tool/sogou.html ================================================ Title ================================================ FILE: tool/youdao.html ================================================ Title