├── .babelrc
├── .editorconfig
├── .gitattributes
├── .gitignore
├── .jshintrc
├── CHANGELOG.md
├── LICENSE
├── README.md
├── app
├── _locales
│ └── zh_CN
│ │ └── messages.json
├── fonts
│ └── .gitkeep
├── images
│ ├── bg.png
│ ├── icon-128-link.png
│ └── icon-128.png
├── manifest.json
├── pages
│ ├── options.html
│ └── popup.html
├── scripts
│ ├── app
│ │ ├── index.js
│ │ └── migrate-options.js
│ ├── background.js
│ ├── components
│ │ ├── FormGroup.vue
│ │ ├── Loader.vue
│ │ ├── OptionsApp.vue
│ │ ├── PopupApp.vue
│ │ ├── RadiosGroup.vue
│ │ ├── Result.vue
│ │ ├── ResultList.vue
│ │ ├── ResultToast.vue
│ │ ├── RuleList.vue
│ │ └── slider.vue
│ ├── config
│ │ └── defaults.js
│ ├── helpers
│ │ ├── message.js
│ │ ├── selection.js
│ │ ├── tabs.js
│ │ ├── utils.js
│ │ └── wait.js
│ ├── inspector.js
│ ├── mixins
│ │ └── options-loader.js
│ ├── options.js
│ ├── page-dict.js
│ ├── page-fanyi.js
│ ├── page.js
│ ├── popup.js
│ └── translator
│ │ ├── dict.js
│ │ ├── fanyi.js
│ │ └── index.js
└── styles
│ ├── components
│ ├── form-group.scss
│ ├── link-inspect-mode.scss
│ ├── loader.scss
│ ├── radio-group.scss
│ ├── result-list.scss
│ ├── result-toast.scss
│ ├── result.scss
│ └── rule-list.scss
│ ├── mixins
│ ├── fade.scss
│ └── reset.scss
│ ├── options.scss
│ ├── page.scss
│ └── popup.scss
├── gulpfile.babel.js
├── package-lock.json
├── package.json
└── tasks
├── build.js
├── chromereload.js
├── clean.js
├── default.js
├── fonts.js
├── images.js
├── lib
├── applyBrowserPrefixesFor.js
└── args.js
├── locales.js
├── manifest.js
├── pack.js
├── pages.js
├── scripts.js
├── styles.js
└── version.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["env", {
4 | "targets": {
5 | "browsers": "last 2 versions"
6 | },
7 | "useBuiltIns": true,
8 | "debug": false
9 | }]
10 | ],
11 | "ignore": ["node_modules"]
12 | }
13 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 2
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [*.md]
12 | trim_trailing_whitespace = false
13 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | .DS_Store
3 |
4 | node_modules
5 | npm-debug.log
6 |
7 | dist/
8 | packages/
9 |
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "esversion": 6
3 | }
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # 更新记录
2 |
3 | V1.6.1 - 2016-12-03
4 |
5 | - 解决了有道词典调用限制的问题
6 | - 解决翻译中的浮层无法关闭的问题
7 |
8 | V1.6 - 2016-06-17
9 |
10 | - 支持必应翻译
11 |
12 | V1.5.4 - 2016-05-28
13 |
14 | - 解决链接划词模式劫持全局快捷键导致页面链接点击异常的问题
15 | - 链接划词的快捷键更新为 Ctrl+Shift+L
16 |
17 | V1.5.3 - 2016-05-26
18 |
19 | - 解决有时页面冒出一大堆之前的翻译结果的问题 #58
20 |
21 | V1.5.1 - 2016-03-06
22 |
23 | - 解决开启链接划词模式会破坏某些网页布局的问题
24 |
25 | V1.5 - 2015-12-27
26 |
27 | - 使用黑科技重新实现百度词典翻译
28 |
29 | V1.4.2 - 2015-12-19
30 |
31 | - 百度词典API已经停止服务,移除百度翻译
32 |
33 | V1.4 - 2015-12-19
34 |
35 | - 可以手动关闭页面划词翻译的结果面板
36 | - 扩展更新时显示功能更新
37 |
38 | V1.3 - 2015-03-11
39 |
40 | - 添加独立的偏好设定页面,并精简弹出窗口
41 | - 鼠标移上页面翻译结果时,结果面板不消失,移出后重新计时
42 | - 解决页面翻译结果被页内查找框遮盖的问题
43 | - 页面划词翻译结果分为两种显示形式,就近和窗口边缘
44 | - 将链接划词的激活快捷键修改为敲击两次 Caps Lock
45 | - 弹出窗口中支持长文本的翻译
46 | - 添加百度翻译的服务,可以在有道和百度之间切换了
47 |
48 | V1.2 - 2014-05-30
49 |
50 | - 支持对 IFRAME 中的内容进行划词翻译
51 | - 支持对使用了 Turbolinks 技术的网站进行划词翻译
52 | - 支持在划词内容附近显示翻译结果
53 | - 限于有道翻译 API 的使用协议,移除缓存功能
54 |
55 | V1.1 - 2014-02-16
56 |
57 | - 页面划词时,如果一个单词的翻译还未消失,再次对该词划词,不会重复翻译
58 | - 修正部分单词没有查找到翻译仍然显示翻译结果的问题
59 | - 页面划词翻译结果增加透明和淡入淡出效果
60 | - 为翻译结果添加音标(如果有)
61 | - 更新链接文本划词翻译的描述
62 | - 解决频繁划词导致翻译结果跑出屏幕的问题(支持使用滚轮滚动)
63 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2013, GDG Xi'an
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without modification,
5 | are permitted provided that the following conditions are met:
6 |
7 | Redistributions of source code must retain the above copyright notice, this
8 | list of conditions and the following disclaimer.
9 |
10 | Redistributions in binary form must reproduce the above copyright notice, this
11 | list of conditions and the following disclaimer in the documentation and/or
12 | other materials provided with the distribution.
13 |
14 | Neither the name of the {organization} nor the names of its
15 | contributors may be used to endorse or promote products derived from
16 | this software without specific prior written permission.
17 |
18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
穆译 ( Smooth Translator )
2 |
3 | 让你沉浸在阅读中的划词翻译利器
4 | 
5 |
6 | ### 穆译的信条
7 |
8 | > 不要让划词翻译干扰阅读状态
9 |
10 | 所以页面划词翻译的结果不会显示在词语附近遮挡住你的阅读材料,而是显示在页面的右上角,余光扫一眼便立即回到阅读,稍后它便会自己关闭。
11 |
12 | ### 如何使用穆译
13 |
14 | - 如果要查询页面中的某个单词,双击选中,单词的释义会显示在页面右上角,然后自动在几秒后消失(你可以在设置页面调整时间长短)。
15 | - 要前往扩展设置页面,可以点击扩展图标打开翻译窗口,然后点击窗口中的齿轮图标。
16 | - 在翻译窗口中也可以进行翻译,输入要翻译的内容,翻译结果会自动呈现。在翻译窗口的输入框中按下 `ESC`,会清除当前的输入内容,
17 | 方便查询新的单词。如果输入框没有内容,`ESC` 会关闭翻译窗口。
18 | - 如果想在某个网站上禁用划词翻译,可以在翻译窗口中取消勾选「在 xxx 启用划词翻译」,
19 | 如果想在全部页面禁用划词翻译,请在设置页面中的取消勾选「默认」条目。
20 | - 如果你想对页面中的某个链接中的单词划词翻译,可以使用快捷键 `Ctrl + Shift + L` 进入链接划词模式,
21 | 此时所有的链接的跳转会失效,你可以对其进行划词翻译,划词后,会自动退出链接划词模式。
22 | 这个快捷键你可以在 Chrome 的快捷键设置里面更改(可以在扩展设置页面点击「更改快捷键」按钮前往设置)。
23 | - 扩展的翻译窗口打开时会自动显示上一次翻译(包括页面划词)的内容,即便你关闭了页面划词翻译,
24 | 仍然可以在页面中选中文本内容后,点击扩展图标打开翻译窗口查看选中内容的翻译结果。
25 | 页面划词翻译只会显示单个英语单词的翻译结果,所以如果你想查看页面上某个句子(包括中文)的翻译结果,
26 | 这个特性会是一个很好的补充。
27 |
28 | ----
29 |
30 | 穆译使用[有道翻译](http://translate.youdao.com/)提供词典和翻译服务。
31 |
32 | 穆译是一个开源应用,如果你想审查代码或者参与开发,请访问
33 |
34 | https://github.com/greatghoul/smooth-translator
35 |
--------------------------------------------------------------------------------
/app/_locales/zh_CN/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "appName": {
3 | "message": "穆译",
4 | "description": "The name of the application"
5 | },
6 | "appShortName": {
7 | "message": "smooth_trans",
8 | "description": "The short_name (maximum of 12 characters recommended) is a short version of the app's name."
9 | },
10 | "appDescription": {
11 | "message": "让你专注于阅读的划词翻译工具",
12 | "description": "The description of the application"
13 | },
14 | "browserActionTitle": {
15 | "message": "穆译",
16 | "description": "The title of the browser action button"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/app/fonts/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/greatghoul/smooth-translator/2e97fb7fada0b0b5602dae2676eee154bbd5ee2c/app/fonts/.gitkeep
--------------------------------------------------------------------------------
/app/images/bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/greatghoul/smooth-translator/2e97fb7fada0b0b5602dae2676eee154bbd5ee2c/app/images/bg.png
--------------------------------------------------------------------------------
/app/images/icon-128-link.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/greatghoul/smooth-translator/2e97fb7fada0b0b5602dae2676eee154bbd5ee2c/app/images/icon-128-link.png
--------------------------------------------------------------------------------
/app/images/icon-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/greatghoul/smooth-translator/2e97fb7fada0b0b5602dae2676eee154bbd5ee2c/app/images/icon-128.png
--------------------------------------------------------------------------------
/app/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 2,
3 | "default_locale": "zh_CN",
4 |
5 | "name": "__MSG_appName__",
6 | "short_name": "__MSG_appShortName__",
7 | "description": "__MSG_appDescription__",
8 | "version": "0.2.0",
9 |
10 | "icons": {
11 | "128": "images/icon-128.png"
12 | },
13 |
14 | "background": {
15 | "scripts": [
16 | "scripts/background.js"
17 | ],
18 | "persistent": true
19 | },
20 |
21 | "browser_action": {
22 | "default_icon": "images/icon-128.png",
23 | "default_title": "__MSG_browserActionTitle__",
24 | "default_popup": "pages/popup.html"
25 | },
26 |
27 | "options_page": "pages/options.html",
28 |
29 | "content_scripts": [
30 | {
31 | "matches": [""],
32 | "js": ["scripts/page.js"],
33 | "css": ["styles/page.css"]
34 | },
35 | {
36 | "matches": [""],
37 | "js": ["scripts/inspector.js"],
38 | "all_frames": true
39 | },
40 | {
41 | "matches": [ "*://fanyi.youdao.com/*" ],
42 | "js": [ "scripts/page-fanyi.js" ],
43 | "all_frames": true
44 | },
45 | {
46 | "matches": [ "*://dict.youdao.com/*" ],
47 | "js": [ "scripts/page-dict.js" ],
48 | "all_frames": true
49 | }
50 | ],
51 |
52 | "commands": {
53 | "toggle-link-inspect": {
54 | "suggested_key": {
55 | "default": "Ctrl+Shift+L",
56 | "mac": "MacCtrl+Shift+L"
57 | },
58 | "description": "打开/关闭链接划词模式"
59 | }
60 | },
61 |
62 | "permissions": [
63 | "tabs",
64 | "storage",
65 | "notifications",
66 | "webRequest",
67 | "webRequestBlocking",
68 | "*://fanyi.youdao.com/*",
69 | "*://dict.youdao.com/*"
70 | ],
71 |
72 | "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'"
73 | }
74 |
--------------------------------------------------------------------------------
/app/pages/options.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 偏好设定
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/pages/popup.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/app/scripts/app/index.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash'
2 | import storage from 'chrome-storage-wrapper'
3 | import migrateOptions from './migrate-options'
4 |
5 | const defaults = {
6 | notifyTimeout: 5,
7 | siteRules: {
8 | '*': true
9 | },
10 | }
11 |
12 | let options = _.clone(defaults);
13 |
14 | function isSiteEnabled(site) {
15 | const { siteRules } = options;
16 | if (site in siteRules) {
17 | return siteRules[site]
18 | } else {
19 | return siteRules['*']
20 | }
21 | }
22 |
23 | function setOptions(newOptions) {
24 | storage.set(newOptions)
25 | options = newOptions
26 | }
27 |
28 | function getOptions() {
29 | if (_.empty(options)) {
30 | return Promise.resolve(options)
31 | } else {
32 | return storage.getAll()
33 | }
34 | }
35 |
36 | function prepareOptions() {
37 | storage.getAll()
38 | .then(options => migrateOptions(options))
39 | .then(options => _.defaults(options, defaults))
40 | .then(options => setOptions(options))
41 | chrome.storage.onChanged.addListener(() => {
42 | options = getOptions()
43 | })
44 | }
45 |
46 | export default {
47 | isSiteEnabled,
48 | prepareOptions,
49 | }
50 |
--------------------------------------------------------------------------------
/app/scripts/app/migrate-options.js:
--------------------------------------------------------------------------------
1 | // Migrate site rules from array to object.
2 | export default function(options) {
3 | if (options.siteRules instanceof Array) {
4 | const rules = {}
5 | options.siteRules.forEach(x => {
6 | rules[x.site] = x.enabled
7 | })
8 |
9 | options.siteRules = rules
10 | }
11 |
12 | return options;
13 | }
14 |
--------------------------------------------------------------------------------
/app/scripts/background.js:
--------------------------------------------------------------------------------
1 | import storage from 'chrome-storage-wrapper'
2 | import { dispatchMessage } from './helpers/message'
3 | import { getActiveTab } from './helpers/tabs'
4 | import defaults from './config/defaults'
5 | import lscache from 'lscache'
6 | import translator from './translator'
7 | import { trim } from 'lodash'
8 | import app from './app'
9 |
10 | const PAT_WORD = /^[a-z]+('|'s)?$/i
11 |
12 | function translateText (text) {
13 | const sourceText = trim(text)
14 | const cacheKey = `text:v2:${sourceText}`
15 | let result = lscache.get(cacheKey)
16 | return result ? Promise.resolve(result) : translator.translate(sourceText)
17 | }
18 |
19 | function isWord(text) {
20 | return PAT_WORD.test(text)
21 | }
22 |
23 | dispatchMessage({
24 | translate (message, sender, sendResponse) {
25 | storage.get('notifyTimeout').then(options => {
26 | translateText(message.text).then(result => {
27 | if (message.from === 'page') {
28 | result.timeout = options.notifyTimeout
29 | } else {
30 | window.localStorage.setItem('current', message.text)
31 | }
32 |
33 | sendResponse(result)
34 | })
35 | })
36 | },
37 |
38 | selection (message, sender, sendResponse) {
39 | window.localStorage.setItem('current', message.text)
40 |
41 | if (isWord(message.text)) {
42 | getActiveTab(tab => {
43 | if (app.isSiteEnabled(tab.hostname)) {
44 | chrome.tabs.sendMessage(sender.tab.id, {
45 | type: 'translate',
46 | text: message.text
47 | })
48 | }
49 | })
50 | }
51 | },
52 |
53 | current (message, sender, sendResponse) {
54 | sendResponse(window.localStorage.getItem('current'))
55 | },
56 |
57 | linkInspect (message, sender, sendResponse) {
58 | if (message.enabled) {
59 | chrome.browserAction.setIcon({ path: 'images/icon-128-link.png' })
60 | } else {
61 | chrome.browserAction.setIcon({ path: 'images/icon-128.png' })
62 | }
63 | }
64 | })
65 |
66 | // Register command for quick link inspect switch
67 | chrome.commands.onCommand.addListener(command => {
68 | if (command === 'toggle-link-inspect') {
69 | getActiveTab(tab => chrome.tabs.sendMessage(tab.id, { type: 'toggleLink' }))
70 | }
71 | })
72 |
73 | app.prepareOptions()
74 |
--------------------------------------------------------------------------------
/app/scripts/components/FormGroup.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
16 |
--------------------------------------------------------------------------------
/app/scripts/components/Loader.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
--------------------------------------------------------------------------------
/app/scripts/components/OptionsApp.vue:
--------------------------------------------------------------------------------
1 |
2 |
34 |
35 |
36 |
78 |
--------------------------------------------------------------------------------
/app/scripts/components/PopupApp.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
15 |
16 |
17 |
18 |
19 |
20 |
32 |
33 |
34 |
35 |
36 |
129 |
--------------------------------------------------------------------------------
/app/scripts/components/RadiosGroup.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
12 |
13 |
14 |
24 |
--------------------------------------------------------------------------------
/app/scripts/components/Result.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{ result.text }}
4 |
{{ result.phonetic }}
5 |
6 |
7 |
8 |
9 |
14 |
--------------------------------------------------------------------------------
/app/scripts/components/ResultList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
12 |
46 |
--------------------------------------------------------------------------------
/app/scripts/components/ResultToast.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
10 |
51 |
52 |
--------------------------------------------------------------------------------
/app/scripts/components/RuleList.vue:
--------------------------------------------------------------------------------
1 |
2 |
26 |
27 |
28 |
33 |
--------------------------------------------------------------------------------
/app/scripts/components/slider.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 | {{ value }} 秒
9 |
10 |
11 |
12 |
22 |
--------------------------------------------------------------------------------
/app/scripts/config/defaults.js:
--------------------------------------------------------------------------------
1 | export default {
2 | // 页面划词结果显示时间
3 | notifyTimeout: 5,
4 |
5 | // 划词翻译在各个网站上是否开启
6 | siteRules: {
7 | '*': true
8 | },
9 | }
10 |
--------------------------------------------------------------------------------
/app/scripts/helpers/message.js:
--------------------------------------------------------------------------------
1 | export function dispatchMessage(mapping) {
2 | chrome.runtime.onMessage.addListener(
3 | function(message, sender, sendResponse) {
4 | const handler = mapping[message.type];
5 | if (typeof handler == 'function') {
6 | handler(message, sender, sendResponse);
7 | }
8 |
9 | return true;
10 | }
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/app/scripts/helpers/selection.js:
--------------------------------------------------------------------------------
1 | import { trim } from 'lodash';
2 |
3 | export function getSelection(evt) {
4 | const selection = window.getSelection()
5 |
6 | return trim(selection.toString())
7 | }
8 |
9 | export function clearSelection() {
10 | const selection = window.getSelection()
11 |
12 | if (selection) {
13 | selection.empty()
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/app/scripts/helpers/tabs.js:
--------------------------------------------------------------------------------
1 | import URL from 'url-parse';
2 |
3 | function getHostname(url) {
4 | const parsed = new URL(url);
5 |
6 | if (parsed) {
7 | return parsed.hostname;
8 | } else {
9 | return '*';
10 | }
11 | }
12 |
13 | export function getActiveTab(callback) {
14 | chrome.tabs.query({ active: true }, tabs => {
15 | const tab = tabs[0];
16 | const hostname = getHostname(tab.url);
17 |
18 | callback(Object.assign({}, tab, { hostname }));
19 | });
20 | }
21 |
--------------------------------------------------------------------------------
/app/scripts/helpers/utils.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash'
2 | import $ from 'jquery'
3 |
4 | export function openExtensionPage(filename) {
5 | var optionsUrl = chrome.extension.getURL(filename)
6 |
7 | chrome.tabs.query({}, function(tabs) {
8 | var optionTab = _.find(tabs, { url: optionsUrl })
9 |
10 | if (optionTab) {
11 | chrome.tabs.reload(optionTab.id)
12 | chrome.tabs.update(optionTab.id, { highlighted: true })
13 | } else {
14 | chrome.tabs.create({ url: optionsUrl })
15 | }
16 | })
17 | }
18 |
19 | export function renderTranslation(query, result) {
20 | let phonetic = ''
21 | let translation = '未找到释义'
22 | let className = 'cst-warning'
23 |
24 | if (result) {
25 | phonetic = result.phonetic
26 | translation = result.translation
27 | className = 'cst-success'
28 | }
29 |
30 | return `
31 |
32 |
${query}
33 |
${phonetic || ''}
34 |
${translation}
35 |
36 | `
37 | }
38 |
39 | function getClientHeight() {
40 | const bodyHeight = document.body.clientHeight
41 | const docHeight = document.documentElement.clientHeight
42 |
43 | let clientHeight = bodyHeight < docHeight ? bodyHeight : docHeight
44 | if (clientHeight === 0) {
45 | clientHeight = docHeight
46 | }
47 |
48 | return clientHeight
49 | }
50 |
51 | function getPosition(evt, selection) {
52 | let rect = selection.getRangeAt(0).getBoundingClientRect()
53 |
54 | // Use mouse position if selection range position invalid (in text field)
55 | if (rect.left === 0 && rect.top === 0) {
56 | rect = { left: evt.clientX, top: evt.clientY, height: 15 }
57 | }
58 |
59 | const left = rect.left + document.body.scrollLeft
60 | const top = rect.top + document.body.scrollTop
61 | const position = { left }
62 |
63 | if (rect.top >= 150) {
64 | position.bottom = getClientHeight() - top
65 | } else {
66 | position.top = top + rect.height + 5
67 | }
68 |
69 | return position
70 | }
71 |
72 | export function stopPropagation(event) {
73 | event.stopPropagation()
74 | }
75 |
76 | // TODO: Move toggleLinkInspectMode function to a proper place
77 | export function toggleLinkInspectMode (flag) {
78 | $('body').toggleClass('cst-link-inspect-mode', flag)
79 | const enabled = $('body').is('.cst-link-inspect-mode')
80 | chrome.runtime.sendMessage({ type: 'linkInspect', enabled })
81 | }
--------------------------------------------------------------------------------
/app/scripts/helpers/wait.js:
--------------------------------------------------------------------------------
1 | import waitUntil from 'wait-until-promise'
2 |
3 | export default function (escapeFunction) {
4 | return waitUntil(escapeFunction, 1000 * 10, 1)
5 | }
6 |
--------------------------------------------------------------------------------
/app/scripts/inspector.js:
--------------------------------------------------------------------------------
1 | import { dispatchMessage } from './helpers/message'
2 | import { getSelection } from './helpers/selection'
3 | import { toggleLinkInspectMode } from './helpers/utils'
4 |
5 | function selectionHandler(evt) {
6 | toggleLinkInspectMode(false)
7 |
8 | const text = getSelection()
9 |
10 | if (text) {
11 | chrome.runtime.sendMessage({ type: 'selection', text: text })
12 | }
13 | }
14 |
15 | document.addEventListener('mouseup', selectionHandler)
16 |
--------------------------------------------------------------------------------
/app/scripts/mixins/options-loader.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | import storage from 'chrome-storage-wrapper';
3 | import defaults from '../config/defaults';
4 |
5 | export default {
6 | data() {
7 | return {
8 | options: Object.assign({}, defaults),
9 | };
10 | },
11 | methods: {
12 | initOptions() {
13 | storage.addChangeListener(() => this.loadOptions());
14 | storage.getAll().then(options => console.log(options));
15 | return this.loadOptions();
16 | },
17 | loadOptions() {
18 | return storage.getAll().then(options => this.options = options);
19 | },
20 | updateOption(name, value) {
21 | this.options[name] = value;
22 | storage.set(name, value);
23 | },
24 | saveRule(rule) {
25 | this.options.siteRules[rule.site] = rule.enabled
26 | this.updateOption('siteRules', this.options.siteRules);
27 | },
28 | removeRule(rule) {
29 | _.remove(this.options.siteRules, { site: rule.site });
30 | this.updateOption('siteRules', this.options.siteRules);
31 | }
32 | },
33 | };
34 |
--------------------------------------------------------------------------------
/app/scripts/options.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import OptionsApp from './components/OptionsApp.vue';
3 |
4 | new Vue({
5 | el: '#app',
6 | render: h => h(OptionsApp),
7 | });
8 |
--------------------------------------------------------------------------------
/app/scripts/page-dict.js:
--------------------------------------------------------------------------------
1 | import { trim } from 'lodash'
2 | import wait from './helpers/wait'
3 |
4 | function fetchResult () {
5 | const elemTrans = document.querySelector('.trans-container')
6 | if (elemTrans && !elemTrans.getAttribute('id')) {
7 | const result = {
8 | status: 'success',
9 | translation: trim(elemTrans.innerHTML)
10 | }
11 |
12 | const elemPhon = document.querySelector('.baav')
13 | if (elemPhon) {
14 | result.phonetic = trim(elemPhon.innerText)
15 | }
16 |
17 | return result
18 | }
19 | }
20 |
21 | function onMessage (event) {
22 | const { data } = event
23 | console.log('[dict] iframe received message:', JSON.stringify(data))
24 | if (data.type === 'fetch-result') {
25 | wait(fetchResult).then(result => {
26 | event.source.postMessage({
27 | type: 'result',
28 | url: data.url,
29 | result: result
30 | }, '*')
31 | })
32 | }
33 | }
34 |
35 | window.addEventListener('message', onMessage, false)
36 |
--------------------------------------------------------------------------------
/app/scripts/page-fanyi.js:
--------------------------------------------------------------------------------
1 | import { trim } from 'lodash'
2 | import wait from './helpers/wait'
3 |
4 | function inputSource (text) {
5 | document.querySelector('#inputOriginal').value = text
6 | }
7 |
8 | function clickSubmit () {
9 | const event = new window.MouseEvent('click', {
10 | bubbles: true,
11 | cancelable: true,
12 | view: window
13 | })
14 |
15 | document.querySelector('#transMachine').dispatchEvent(event)
16 | }
17 |
18 | function fetchResult () {
19 | const elem = document.querySelector('#transTarget')
20 | return trim(elem.innerText)
21 | }
22 |
23 | function onMessage (event) {
24 | const { data } = event
25 | if (data.type === 'fetch-result') {
26 | console.log('[fanyi] iframe received message', JSON.stringify(data))
27 | inputSource(data.text)
28 | clickSubmit()
29 |
30 | wait(fetchResult).then(result => {
31 | event.source.postMessage({
32 | type: 'result',
33 | token: data.token,
34 | result: { translation: result, status: 'success' }
35 | }, '*')
36 | })
37 | }
38 | }
39 |
40 | window.addEventListener('message', onMessage, false)
41 |
--------------------------------------------------------------------------------
/app/scripts/page.js:
--------------------------------------------------------------------------------
1 | import $ from 'jquery'
2 | import Vue from 'vue'
3 | import { dispatchMessage } from './helpers/message'
4 | import { toggleLinkInspectMode } from './helpers/utils'
5 | import ResultList from './components/ResultList.vue'
6 |
7 | let app = null;
8 |
9 | function getApp () {
10 | if ($('#cst-list').length == 0) {
11 | $('').appendTo('body')
12 | app = new Vue({
13 | el: '#cst-list',
14 | render: h => h(ResultList),
15 | });
16 | }
17 |
18 | return app.$children[0]
19 | }
20 |
21 | function translate (message, sender, sendResponse) {
22 | getApp().translate(message.text)
23 | }
24 |
25 | function toggleLink (message, sender, sendResponse) {
26 | toggleLinkInspectMode()
27 | }
28 |
29 | dispatchMessage({ translate, toggleLink })
30 |
--------------------------------------------------------------------------------
/app/scripts/popup.js:
--------------------------------------------------------------------------------
1 | import 'vue-awesome/icons/cog'
2 |
3 | import Vue from 'vue'
4 | import Icon from 'vue-awesome/components/Icon'
5 | import PopupApp from './components/PopupApp.vue'
6 |
7 | Vue.component('icon', Icon)
8 |
9 | new Vue({
10 | el: '#app',
11 | render: h => h(PopupApp),
12 | })
13 |
--------------------------------------------------------------------------------
/app/scripts/translator/dict.js:
--------------------------------------------------------------------------------
1 | import wait from '../helpers/wait'
2 |
3 | const URL = 'http://dict.youdao.com'
4 |
5 | export default class Dict {
6 | destroy () {
7 | window.removeEventListener('mousedown', this.onMessage, false)
8 | this.iframe.remove()
9 | }
10 |
11 | register () {
12 | window.addEventListener('message', this.onMessage, false)
13 | this.iwindow.postMessage({ type: 'fetch-result', url: this.url }, '*')
14 | }
15 |
16 | receive (event) {
17 | const { data } = event
18 | console.log('[dict] event received message:', JSON.stringify(data))
19 | if (data.type === 'result' && data.url === this.url) {
20 | this.result = data.result
21 | }
22 | }
23 |
24 | get iwindow () {
25 | return this.iframe.contentWindow
26 | }
27 |
28 | fetchResult () {
29 | return this.result
30 | }
31 |
32 | translate (text) {
33 | this.url = `${URL}/w/${encodeURIComponent(text)}/#keyfrom=dict2.top`
34 | this.iframe = document.createElement('iframe')
35 | document.body.appendChild(this.iframe)
36 | this.iframe.onload = () => {
37 | this.register()
38 | }
39 | this.onMessage = this.receive.bind(this)
40 | this.result = null
41 | this.iframe.src = this.url
42 | return wait(() => this.fetchResult())
43 | }
44 | }
45 |
46 | Dict.translate = function (text) {
47 | let dict = new Dict()
48 | return dict.translate(text).finally(() => {
49 | dict.destroy()
50 | dict = null
51 | })
52 | }
53 |
--------------------------------------------------------------------------------
/app/scripts/translator/fanyi.js:
--------------------------------------------------------------------------------
1 | import wait from '../helpers/wait'
2 |
3 | const URL = 'http://fanyi.youdao.com/?keyfrom=dict2.index'
4 |
5 | export default class Fanyi {
6 | destroy () {
7 | window.removeEventListener('mousedown', this.onMessage, false)
8 | this.iframe.remove()
9 | }
10 |
11 | register () {
12 | window.addEventListener('message', this.onMessage, false)
13 | this.iwindow.postMessage({
14 | type: 'fetch-result',
15 | token: this.token,
16 | text: this.text
17 | }, '*')
18 | }
19 |
20 | receive (event) {
21 | const { data } = event
22 | if (data.type === 'result' && data.token === this.token) {
23 | console.log('[fanyi] event received message:', JSON.stringify(data))
24 | this.result = data.result
25 | }
26 | }
27 |
28 | get iwindow () {
29 | return this.iframe.contentWindow
30 | }
31 |
32 | fetchResult () {
33 | return this.result
34 | }
35 |
36 | translate (text) {
37 | this.text = text
38 | this.token = 'fanyi-' + Date.now()
39 | this.iframe = document.createElement('iframe')
40 | document.body.appendChild(this.iframe)
41 | this.iframe.onload = () => {
42 | this.register()
43 | }
44 | this.onMessage = this.receive.bind(this)
45 | this.result = null
46 | this.iframe.src = URL
47 | return wait(() => this.fetchResult())
48 | }
49 | }
50 |
51 | Fanyi.translate = function (text) {
52 | let fanyi = new Fanyi()
53 | return fanyi.translate(text).finally(() => {
54 | fanyi.destroy()
55 | fanyi = null
56 | })
57 | }
58 |
--------------------------------------------------------------------------------
/app/scripts/translator/index.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash'
2 | import lscache from 'lscache'
3 | import Dict from './dict'
4 | import Fanyi from './fanyi'
5 | import { words } from 'lodash'
6 |
7 | const PAT_WORD = /^([a-z]+-?)+$/i
8 | const RESULT_FAILURE = {
9 | translation: '未找到释义',
10 | status: 'failure'
11 | }
12 |
13 | function isWord (text) {
14 | return text.match(PAT_WORD)
15 | }
16 |
17 | function smartText (text) {
18 | return isWord(text) ? words(text).join(' ') : text
19 | }
20 |
21 | function cacheResult(text, result) {
22 | const key = `text:v2:${_.trim(text)}`
23 | lscache.set(key, result, 60 * 24 * 7)
24 | return result
25 | }
26 |
27 | function translate (text) {
28 | const sourceText = smartText(text)
29 |
30 | if (!sourceText) {
31 | Promise.resolve(RESULT_FAILURE)
32 | } else if (isWord(sourceText)) {
33 | return Dict.translate(sourceText)
34 | .then(result => cacheResult(text, result))
35 | .catch(() => RESULT_FAILURE)
36 | } else {
37 | return Fanyi.translate(sourceText).catch(() => RESULT_FAILURE)
38 | }
39 | }
40 |
41 | export default { translate }
42 |
--------------------------------------------------------------------------------
/app/styles/components/form-group.scss:
--------------------------------------------------------------------------------
1 | .form-group {
2 | margin-bottom: 15px;
3 | padding-bottom: 15px;
4 | border-bottom: 1px solid #f0f0f0;
5 | &:last-child {
6 | border-bottom: 0;
7 | margin-bottom: 0;
8 | padding-bottom: 0;
9 | }
10 | .control-label {
11 | margin: 3px 0;
12 | float: left;
13 | width: 200px;
14 | font-weight: bold;
15 | font-size: 1.1em;
16 | }
17 | .controls {
18 | margin-left: 200px;
19 | .hint {
20 | color: #aaaaaa;
21 | margin-top: 10px;
22 | margin-bottom: 0;
23 | }
24 | }
25 | }
--------------------------------------------------------------------------------
/app/styles/components/link-inspect-mode.scss:
--------------------------------------------------------------------------------
1 | body.cst-link-inspect-mode a {
2 | text-decoration: none !important;
3 | pointer-events: none !important;
4 | }
5 |
--------------------------------------------------------------------------------
/app/styles/components/loader.scss:
--------------------------------------------------------------------------------
1 | $color_loader_dark: #8fc2e4;
2 | $color_loader_light: #eeeeee;
3 |
4 | @keyframes progress {
5 | 0% {
6 | transform: translateX(-240px);
7 | opacity: 0;
8 | }
9 |
10 | 50% {
11 | opacity: 1;
12 | }
13 |
14 | 100% {
15 | transform: translateX(240px);
16 | opacity: 0;
17 | }
18 | }
19 |
20 | .loader {
21 | position: absolute;
22 | top: 0;
23 | left: 0;
24 | height: 2px;
25 |
26 | .progress {
27 | position: absolute;
28 | width: 120px;
29 | height: 2px;
30 | line-height: 2px;
31 | background-image: linear-gradient(-45deg, $color_loader_light, $color_loader_dark );
32 | background-size: 120px 2px;
33 | animation: progress 1.8s ease-in-out 0s infinite;
34 | }
35 | }
--------------------------------------------------------------------------------
/app/styles/components/radio-group.scss:
--------------------------------------------------------------------------------
1 | .radio-inline {
2 | display: inline-block;
3 | margin-right: 5px;
4 | }
5 |
--------------------------------------------------------------------------------
/app/styles/components/result-list.scss:
--------------------------------------------------------------------------------
1 | .cst-list {
2 | @include reset;
3 | }
4 |
5 | #cst-list {
6 | position: fixed;
7 | z-index: 2147483647;
8 | width: 250px;
9 | right: 15px;
10 | top: 35px;
11 | max-height: calc(100% - 40px);
12 | overflow: auto;
13 |
14 | &::-webkit-scrollbar {
15 | display: none;
16 | width: 0px;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/app/styles/components/result-toast.scss:
--------------------------------------------------------------------------------
1 | #cst-list .cst-result-toast {
2 | @include fade;
3 |
4 | z-index: 2147483647;
5 | max-width: 250px;
6 | min-width: 150px;
7 | line-height: 1.5;
8 | font-size: 14px;
9 | margin-bottom: 5px;
10 | border-radius: 5px;
11 | box-shadow: 3px 3px 3px #000000;
12 | opacity: 0.9;
13 |
14 | a.close {
15 | float: right;
16 | text-decoration: none;
17 | margin-right: 5px;
18 | font-size: .8em;
19 | color: #cecece;
20 |
21 | &:hover {
22 | color: #ffffff;
23 | }
24 | }
25 | }
--------------------------------------------------------------------------------
/app/styles/components/result.scss:
--------------------------------------------------------------------------------
1 | #cst-list .cst-result,
2 | .cst-result {
3 | padding: 3px 5px;
4 |
5 | .cst-result-phonetic {
6 | margin: 0 0 5px 0;
7 | }
8 |
9 | h6, code, pre {
10 | border: none;
11 | background: none;
12 | color: inherit;
13 | padding: 0;
14 | margin: 0;
15 | text-transform: none;
16 | font-size: 14px;
17 | line-height: 20px;
18 | white-space: normal;
19 | }
20 |
21 | .cst-result-text {
22 | margin-bottom: 5px;
23 | font-weight: 600;
24 | }
25 |
26 | .cst-result-phonetic {
27 | padding-top: 5px !important;
28 | padding-bottom: 10px !important;
29 | color: #58afb1;
30 | }
31 |
32 | .cst-result-translation {
33 | overflow-y: auto;
34 | max-height: 200px;
35 |
36 | .additional {
37 | color: #aaa !important;
38 | font-size: 0.9em !important;
39 | margin-top: 5px !important;
40 | padding-bottom: 5px !important;
41 | }
42 | }
43 | }
44 |
45 | .cst-result[data-cst-theme="dark"] {
46 | &[data-cst-status="success"],
47 | &[data-cst-status="pending"] {
48 | background: #336721;
49 | color: #EDF8ED;
50 | }
51 |
52 | &[data-cst-status="failure"] {
53 | background: #FFF299;
54 | color: #888888;
55 | }
56 | }
57 |
58 | .cst-result[data-cst-theme="light"] {
59 | &[data-cst-status="success"],
60 | &[data-cst-status="pending"] {
61 | background: #DDEADD;
62 | color: #2B3F29;
63 | }
64 |
65 | &[data-cst-status="failure"] {
66 | background: #fff3c8;
67 | color: #888888;
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/app/styles/components/rule-list.scss:
--------------------------------------------------------------------------------
1 | table.table-rules {
2 | width: 100%;
3 | border: 2px solid #ddd;
4 |
5 | th, td {
6 | padding: 5px 10px;
7 | text-align: center;
8 |
9 | &:first-child {
10 | text-align: left;
11 | }
12 | }
13 |
14 | th {
15 | background-color: #ddd;
16 | }
17 |
18 | tbody tr:not(:first-child) td {
19 | border-top: 1px solid #ddd;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/app/styles/mixins/fade.scss:
--------------------------------------------------------------------------------
1 | @mixin fade {
2 | &.fade-enter-active, &.fade-leave-active {
3 | transition: opacity .5s;
4 | }
5 | &.fade-enter, &.fade-leave-active {
6 | opacity: 0;
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/app/styles/mixins/reset.scss:
--------------------------------------------------------------------------------
1 | @mixin reset {
2 | &, & * {
3 | margin: 0;
4 | padding: 0;
5 | border: 0;
6 | font-size: 100%;
7 | font-family: "Helvetica Neue", "Luxi Sans", "DejaVu Sans", Tahoma, "Hiragino Sans GB", "Microsoft Yahei", sans-serif;
8 | vertical-align: baseline;
9 | width: auto;
10 | height: auto;
11 | background: none;
12 | border-radius: 0;
13 | text-align: left;
14 | }
15 | }
--------------------------------------------------------------------------------
/app/styles/options.scss:
--------------------------------------------------------------------------------
1 | @import 'components/form-group';
2 | @import 'components/rule-list';
3 |
4 | body {
5 | background: #efefe9;
6 | margin: 0;
7 | padding: 0;
8 | color: #555;
9 | font-size: 13px;
10 | }
11 |
12 | .board {
13 | width: 65%;
14 | min-width: 800px;
15 | margin: 60px auto;
16 | background: #fff;
17 |
18 | .board-header {
19 | background: #fafafa url('../../images/bg.png');
20 | background-size: 30%;
21 | border-bottom: 1px solid #eeeeee;
22 | padding: 10px 30px;
23 |
24 | .title {
25 | margin-top: 10px;
26 | margin-bottom: 10px;
27 | font-size: 30px;
28 | line-height: 48px;
29 | display: inline-block;
30 | }
31 |
32 | img {
33 | vertical-align: middle;
34 | }
35 | }
36 | }
37 |
38 | .board-content {
39 | padding: 30px;
40 | }
41 |
42 | input[type=range] {
43 | vertical-align: middle;
44 | }
45 |
46 | .command {
47 | padding: 2px 4px;
48 | margin: 0 5px;
49 | font-size: 1.2em;
50 | color: #c7254e;
51 | background-color: #f9f2f4;
52 | border-radius: 4px;
53 | }
--------------------------------------------------------------------------------
/app/styles/page.scss:
--------------------------------------------------------------------------------
1 | @import 'mixins/reset';
2 | @import 'mixins/fade';
3 | @import 'components/result-list';
4 | @import 'components/result-toast';
5 | @import 'components/result';
6 | @import 'components/link-inspect-mode';
7 |
8 |
--------------------------------------------------------------------------------
/app/styles/popup.scss:
--------------------------------------------------------------------------------
1 | $font-color: #555;
2 |
3 | @import 'mixins/reset';
4 | @import 'components/loader';
5 | @import 'components/result';
6 |
7 | body {
8 | width: 240px;
9 | min-height: 85px;
10 | margin: 0;
11 | font-size: 14px;
12 | color: $font-color;
13 | background-color: #F2F5F6;
14 | }
15 |
16 | [v-cloak] {
17 | visibility: hidden;
18 | }
19 |
20 | .translator {
21 | .input-box {
22 | padding: 5px 5px 2px 5px;;
23 |
24 | textarea {
25 | -webkit-appearance: textfield;
26 | border: 1px inset #e0e0e0;
27 | background-color: #fefbf5;
28 | resize: none;
29 | font-size: 12px;
30 | line-height: 1.2em;
31 | color: #888;
32 | width: 100%;
33 | margin: 0;
34 | font-weight: bold;
35 | box-sizing: border-box;
36 |
37 | &:active, &:focus {
38 | outline: none;
39 | background-color: #ffffd4;
40 | }
41 | }
42 | }
43 |
44 | .result-wrapper {
45 | @include reset;
46 | padding: 0px 5px !important;
47 |
48 | .cst-result {
49 | padding: 0 5px;
50 | }
51 | }
52 |
53 | footer {
54 | height: 24px;
55 | line-height: 24px;
56 | padding: 0 5px;
57 |
58 | .btn-settings {
59 | float: right;
60 |
61 | .fa-icon {
62 | fill: $font-color;
63 | margin-top: 5px;
64 | vertical-align: top;
65 | width: auto;
66 | height: 1em;
67 | max-width: 100%;
68 | max-height: 100%;
69 | }
70 | }
71 |
72 | label {
73 | .site {
74 | font-style: italic;
75 | font-weight: bold;
76 | font-size: .9em;
77 | display: inline-block;
78 | vertical-align: bottom;
79 | max-width: 50px;
80 | overflow: hidden;
81 | text-overflow: ellipsis;
82 | white-space: nowrap;
83 | }
84 | }
85 | }
86 | }
87 |
88 | label {
89 | font-size: 0.9em;
90 | color: gray;
91 | user-select: none;
92 |
93 | &.enabled {
94 | color: green;
95 | }
96 |
97 | input[type="checkbox"] {
98 | margin: 0;
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/gulpfile.babel.js:
--------------------------------------------------------------------------------
1 | import requireDir from 'require-dir'
2 |
3 | // Check out the tasks directory
4 | // if you want to modify tasks!
5 | requireDir('./tasks')
6 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "smooth-translator",
3 | "private": true,
4 | "version": "0.2.0",
5 | "scripts": {
6 | "start": "npm run dev:chrome",
7 | "build": "npm run build:chrome",
8 | "build:chrome": "gulp pack --production --vendor=chrome",
9 | "build:firefox": "gulp pack --production --vendor=firefox",
10 | "build:opera": "gulp pack --production --vendor=opera",
11 | "build:edge": "gulp pack --production --vendor=edge",
12 | "dev": "npm run dev:chrome",
13 | "dev:chrome": "gulp --watch --vendor=chrome",
14 | "dev:firefox": "gulp --watch --vendor=firefox",
15 | "dev:opera": "gulp --watch --vendor=opera",
16 | "dev:edge": "gulp --watch --vendor=edge",
17 | "lint": "standard"
18 | },
19 | "standard": {
20 | "globals": [
21 | "chrome"
22 | ]
23 | },
24 | "devDependencies": {
25 | "babel-cli": "^6.26.0",
26 | "babel-core": "^6.26.3",
27 | "babel-loader": "7.x.x",
28 | "babel-preset-env": "^1.7.0",
29 | "chai": "4.x.x",
30 | "chromereload": "0.x.x",
31 | "debounce": "1.x.x",
32 | "del": "3.x.x",
33 | "gulp": "3.x.x",
34 | "gulp-bump": "2.x.x",
35 | "gulp-cache": "0.x.x",
36 | "gulp-clean-css": "^3.x.x",
37 | "gulp-filter": "^5.x.x",
38 | "gulp-git": "^2.x.x",
39 | "gulp-if": "2.x.x",
40 | "gulp-imagemin": "3.x.x",
41 | "gulp-json-transform": "0.x.x",
42 | "gulp-less": "3.x.x",
43 | "gulp-livereload": "3.x.x",
44 | "gulp-plumber": "1.x.x",
45 | "gulp-sass": "^3.x.x",
46 | "gulp-sequence": "0.x.x",
47 | "gulp-sourcemaps": "^2.x.x",
48 | "gulp-tag-version": "1.x.x",
49 | "gulp-util": "3.x.x",
50 | "gulp-zip": "^4.x.x",
51 | "require-dir": "1.x.x",
52 | "standard": "^10.0.2",
53 | "vinyl-named": "1.x.x",
54 | "vue-loader": "^13.3.0",
55 | "vue-template-compiler": "^2.5.2",
56 | "webpack": "3.x.x",
57 | "webpack-stream": "3.x.x",
58 | "yargs": "^8.x.x"
59 | },
60 | "dependencies": {
61 | "babel-preset-es2015": "^6.24.1",
62 | "chrome-storage-wrapper": "^0.1.4",
63 | "css-loader": "^1.0.0",
64 | "jquery": "^3.3.1",
65 | "lodash": "^4.17.10",
66 | "lscache": "^1.1.0",
67 | "url-parse": "^1.4.3",
68 | "vue": "^2.5.2",
69 | "vue-awesome": "^2.3.3",
70 | "wait-until-promise": "^1.0.0"
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/tasks/build.js:
--------------------------------------------------------------------------------
1 | import gulp from 'gulp'
2 | import gulpSequence from 'gulp-sequence'
3 |
4 | gulp.task('build', gulpSequence(
5 | 'clean', [
6 | 'manifest',
7 | 'scripts',
8 | 'styles',
9 | 'pages',
10 | 'locales',
11 | 'images',
12 | 'fonts',
13 | 'chromereload'
14 | ]
15 | ))
16 |
--------------------------------------------------------------------------------
/tasks/chromereload.js:
--------------------------------------------------------------------------------
1 | import gulp from 'gulp'
2 | import gutil from 'gulp-util'
3 | import livereload from 'gulp-livereload'
4 | import args from './lib/args'
5 |
6 | // In order to make chromereload work you'll need to include
7 | // the following line in your `scipts/background.js` file.
8 | //
9 | // import 'chromereload/devonly';
10 | //
11 | // This will reload your extension everytime a file changes.
12 | // If you just want to reload a specific context of your extension
13 | // (e.g. `pages/options.html`) include the script in that context
14 | // (e.g. `scripts/options.js`).
15 | //
16 | // Please note that you'll have to restart the gulp task if you
17 | // create new file. We'll fix that when gulp 4 comes out.
18 |
19 | gulp.task('chromereload', (cb) => {
20 | // This task runs only if the
21 | // watch argument is present!
22 | if (!args.watch) return cb()
23 |
24 | // Start livereload server
25 | livereload.listen({
26 | reloadPage: 'Extension',
27 | quiet: !args.verbose
28 | })
29 |
30 | gutil.log('Starting', gutil.colors.cyan('\'livereload-server\''))
31 |
32 | // The watching for javascript files is done by webpack
33 | // Check out ./tasks/scripts.js for further info.
34 | gulp.watch('app/manifest.json', ['manifest'])
35 | gulp.watch('app/styles/**/*.css', ['styles:css'])
36 | gulp.watch('app/styles/**/*.less', ['styles:less'])
37 | gulp.watch('app/styles/**/*.scss', ['styles:sass'])
38 | gulp.watch('app/pages/**/*.html', ['pages'])
39 | gulp.watch('app/_locales/**/*', ['locales'])
40 | gulp.watch('app/images/**/*', ['images'])
41 | gulp.watch('app/fonts/**/*.{woff,ttf,eot,svg}', ['fonts'])
42 | })
43 |
--------------------------------------------------------------------------------
/tasks/clean.js:
--------------------------------------------------------------------------------
1 | import gulp from 'gulp'
2 | import del from 'del'
3 | import args from './lib/args'
4 |
5 | gulp.task('clean', () => {
6 | return del(`dist/${args.vendor}/**/*`)
7 | })
8 |
--------------------------------------------------------------------------------
/tasks/default.js:
--------------------------------------------------------------------------------
1 | import gulp from 'gulp'
2 |
3 | gulp.task('default', ['build'])
4 |
--------------------------------------------------------------------------------
/tasks/fonts.js:
--------------------------------------------------------------------------------
1 | import gulp from 'gulp'
2 | import gulpif from 'gulp-if'
3 | import livereload from 'gulp-livereload'
4 | import args from './lib/args'
5 |
6 | gulp.task('fonts', () => {
7 | return gulp.src('app/fonts/**/*.{woff,woff2,ttf,eot,svg}')
8 | .pipe(gulp.dest(`dist/${args.vendor}/fonts`))
9 | .pipe(gulpif(args.watch, livereload()))
10 | })
11 |
--------------------------------------------------------------------------------
/tasks/images.js:
--------------------------------------------------------------------------------
1 | import gulp from 'gulp'
2 | import gulpif from 'gulp-if'
3 | import imagemin from 'gulp-imagemin'
4 | import livereload from 'gulp-livereload'
5 | import args from './lib/args'
6 |
7 | gulp.task('images', () => {
8 | return gulp.src('app/images/**/*')
9 | .pipe(gulpif(args.production, imagemin()))
10 | .pipe(gulp.dest(`dist/${args.vendor}/images`))
11 | .pipe(gulpif(args.watch, livereload()))
12 | })
13 |
--------------------------------------------------------------------------------
/tasks/lib/applyBrowserPrefixesFor.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Converts and removes keys with a
3 | * browser prefix to the key without prefix
4 | *
5 | * Example:
6 | *
7 | * __chrome__keyName
8 | * __firefox__keyName
9 | * __opera__keyName
10 | * __edge__keyName
11 | *
12 | * to `keyName`.
13 | * This way we can write one manifest thats valid
14 | * for all browsers
15 | *
16 | * @param {Object} manifest
17 | * @return {Object}
18 | */
19 | export default function applyBrowserPrefixesFor (_vendor) {
20 | vendor = _vendor
21 | return iterator
22 | };
23 |
24 | /**
25 | * Vendor key
26 | * @type {String}
27 | */
28 | var vendor = ''
29 |
30 | /**
31 | * Recursive iterator over all object keys
32 | * @param {Object} obj Object to iterate over
33 | * @return {Object} Processed object
34 | */
35 | function iterator (obj) {
36 | Object.keys(obj).forEach((key) => {
37 | let match = key.match(/^__(chrome|firefox|opera|edge)__(.*)/)
38 | if (match) {
39 | // Swap key with non prefixed name
40 | if (match[1] === vendor) {
41 | obj[match[2]] = obj[key]
42 | }
43 |
44 | // Remove the prefixed key
45 | // so it won't cause warings
46 | delete obj[key]
47 | } else { // no match? try deeper
48 | // Recurse over object's inner keys
49 | if (typeof (obj[key]) === 'object') iterator(obj[key])
50 | }
51 | })
52 | return obj
53 | }
54 |
--------------------------------------------------------------------------------
/tasks/lib/args.js:
--------------------------------------------------------------------------------
1 | import yargs from 'yargs'
2 |
3 | const args = yargs
4 |
5 | .option('production', {
6 | boolean: true,
7 | default: false,
8 | describe: 'Minify all scripts and assets'
9 | })
10 |
11 | .option('watch', {
12 | boolean: true,
13 | default: false,
14 | describe: 'Watch all files and start a livereload server'
15 | })
16 |
17 | .option('verbose', {
18 | boolean: true,
19 | default: false,
20 | describe: 'Log additional data'
21 | })
22 |
23 | .option('vendor', {
24 | string: true,
25 | default: 'chrome',
26 | describe: 'Compile the extension for different vendors',
27 | choices: ['chrome', 'firefox', 'opera', 'edge']
28 | })
29 |
30 | .option('sourcemaps', {
31 | describe: 'Force the creation of sourcemaps'
32 | })
33 |
34 | .argv
35 |
36 | // Use production flag for sourcemaps
37 | // as a fallback
38 | if (typeof args.sourcemaps === 'undefined') {
39 | args.sourcemaps = !args.production
40 | }
41 |
42 | export default args
43 |
--------------------------------------------------------------------------------
/tasks/locales.js:
--------------------------------------------------------------------------------
1 | import gulp from 'gulp'
2 | import gulpif from 'gulp-if'
3 | import livereload from 'gulp-livereload'
4 | import args from './lib/args'
5 |
6 | gulp.task('locales', () => {
7 | return gulp.src('app/_locales/**/*.json')
8 | .pipe(gulp.dest(`dist/${args.vendor}/_locales`))
9 | .pipe(gulpif(args.watch, livereload()))
10 | })
11 |
--------------------------------------------------------------------------------
/tasks/manifest.js:
--------------------------------------------------------------------------------
1 | import gulp from 'gulp'
2 | import gulpif from 'gulp-if'
3 | import { colors, log } from 'gulp-util'
4 | import livereload from 'gulp-livereload'
5 | import jsonTransform from 'gulp-json-transform'
6 | import plumber from 'gulp-plumber'
7 | import applyBrowserPrefixesFor from './lib/applyBrowserPrefixesFor'
8 | import args from './lib/args'
9 |
10 | gulp.task('manifest', () => {
11 | return gulp.src('app/manifest.json')
12 | .pipe(plumber({
13 | errorHandler: error => {
14 | if (error) {
15 | log('manifest:', colors.red('Invalid manifest.json'))
16 | }
17 | }
18 | }))
19 | .pipe(
20 | jsonTransform(
21 | applyBrowserPrefixesFor(args.vendor),
22 | 2 /* whitespace */
23 | )
24 | )
25 | .pipe(gulp.dest(`dist/${args.vendor}`))
26 | .pipe(gulpif(args.watch, livereload()))
27 | })
28 |
--------------------------------------------------------------------------------
/tasks/pack.js:
--------------------------------------------------------------------------------
1 | import gulp from 'gulp'
2 | import { colors, log } from 'gulp-util'
3 | import zip from 'gulp-zip'
4 | import packageDetails from '../package.json'
5 | import args from './lib/args'
6 |
7 | function getPackFileType () {
8 | switch (args.vendor) {
9 | case 'firefox':
10 | return '.xpi'
11 | case 'opera':
12 | return '.crx'
13 | default:
14 | return '.zip'
15 | }
16 | }
17 |
18 | gulp.task('pack', ['build'], () => {
19 | let name = packageDetails.name
20 | let version = packageDetails.version
21 | let filetype = getPackFileType()
22 | let filename = `${name}-${version}-${args.vendor}${filetype}`
23 | return gulp.src(`dist/${args.vendor}/**/*`)
24 | .pipe(zip(filename))
25 | .pipe(gulp.dest('./packages'))
26 | .on('end', () => {
27 | let distStyled = colors.magenta(`dist/${args.vendor}`)
28 | let filenameStyled = colors.magenta(`./packages/${filename}`)
29 | log(`Packed ${distStyled} to ${filenameStyled}`)
30 | })
31 | })
32 |
--------------------------------------------------------------------------------
/tasks/pages.js:
--------------------------------------------------------------------------------
1 | import gulp from 'gulp'
2 | import gulpif from 'gulp-if'
3 | import livereload from 'gulp-livereload'
4 | import args from './lib/args'
5 |
6 | gulp.task('pages', () => {
7 | return gulp.src('app/pages/**/*.html')
8 | .pipe(gulp.dest(`dist/${args.vendor}/pages`))
9 | .pipe(gulpif(args.watch, livereload()))
10 | })
11 |
--------------------------------------------------------------------------------
/tasks/scripts.js:
--------------------------------------------------------------------------------
1 | import gulp from 'gulp'
2 | import gulpif from 'gulp-if'
3 | import { log, colors } from 'gulp-util'
4 | import named from 'vinyl-named'
5 | import webpack from 'webpack'
6 | import gulpWebpack from 'webpack-stream'
7 | import plumber from 'gulp-plumber'
8 | import livereload from 'gulp-livereload'
9 | import args from './lib/args'
10 |
11 | const path = require('path')
12 | const ENV = args.production ? 'production' : 'development'
13 |
14 | gulp.task('scripts', (cb) => {
15 | return gulp.src('app/scripts/*.js')
16 | .pipe(plumber({
17 | // Webpack will log the errors
18 | errorHandler () {}
19 | }))
20 | .pipe(named())
21 | .pipe(gulpWebpack({
22 | devtool: args.sourcemaps ? 'inline-source-map' : false,
23 | watch: args.watch,
24 | plugins: [
25 | new webpack.DefinePlugin({
26 | 'process.env.NODE_ENV': JSON.stringify(ENV),
27 | 'process.env.VENDOR': JSON.stringify(args.vendor)
28 | })
29 | ].concat(args.production ? [
30 | new webpack.optimize.UglifyJsPlugin()
31 | ] : []),
32 | module: {
33 | rules: [
34 | {
35 | test: /\.js$/,
36 | loader: 'babel-loader',
37 | },
38 | {
39 | test: /\.vue$/,
40 | loader: 'vue-loader',
41 | options: {
42 | transformToRequire: {
43 | image: 'xlink:href'
44 | }
45 | }
46 | },
47 | ]
48 | },
49 | resolve: {
50 | alias: {
51 | helpers: path.resolve(__dirname, 'app/scripts/helpers/'),
52 | mixins: path.resolve(__dirname, 'app/scripts/mixins/')
53 | },
54 | extensions: ['.js', '.json', '.vue']
55 | }
56 | },
57 | webpack,
58 | (err, stats) => {
59 | if (err) return
60 | log(`Finished '${colors.cyan('scripts')}'`, stats.toString({
61 | chunks: false,
62 | colors: true,
63 | cached: false,
64 | children: false
65 | }))
66 | }))
67 | .pipe(gulp.dest(`dist/${args.vendor}/scripts`))
68 | .pipe(gulpif(args.watch, livereload()))
69 | })
70 |
--------------------------------------------------------------------------------
/tasks/styles.js:
--------------------------------------------------------------------------------
1 | import gulp from 'gulp'
2 | import gulpif from 'gulp-if'
3 | import gutil from 'gulp-util'
4 | import sourcemaps from 'gulp-sourcemaps'
5 | import less from 'gulp-less'
6 | import sass from 'gulp-sass'
7 | import cleanCSS from 'gulp-clean-css'
8 | import livereload from 'gulp-livereload'
9 | import args from './lib/args'
10 |
11 | gulp.task('styles:css', function () {
12 | return gulp.src('app/styles/*.css')
13 | .pipe(gulpif(args.sourcemaps, sourcemaps.init()))
14 | .pipe(gulpif(args.production, cleanCSS()))
15 | .pipe(gulpif(args.sourcemaps, sourcemaps.write()))
16 | .pipe(gulp.dest(`dist/${args.vendor}/styles`))
17 | .pipe(gulpif(args.watch, livereload()))
18 | })
19 |
20 | gulp.task('styles:less', function () {
21 | return gulp.src('app/styles/*.less')
22 | .pipe(gulpif(args.sourcemaps, sourcemaps.init()))
23 | .pipe(less({ paths: ['./app'] }).on('error', function (error) {
24 | gutil.log(gutil.colors.red('Error (' + error.plugin + '): ' + error.message))
25 | this.emit('end')
26 | }))
27 | .pipe(gulpif(args.production, cleanCSS()))
28 | .pipe(gulpif(args.sourcemaps, sourcemaps.write()))
29 | .pipe(gulp.dest(`dist/${args.vendor}/styles`))
30 | .pipe(gulpif(args.watch, livereload()))
31 | })
32 |
33 | gulp.task('styles:sass', function () {
34 | return gulp.src('app/styles/*.scss')
35 | .pipe(gulpif(args.sourcemaps, sourcemaps.init()))
36 | .pipe(sass({ includePaths: ['./app'] }).on('error', function (error) {
37 | gutil.log(gutil.colors.red('Error (' + error.plugin + '): ' + error.message))
38 | this.emit('end')
39 | }))
40 | .pipe(gulpif(args.production, cleanCSS()))
41 | .pipe(gulpif(args.sourcemaps, sourcemaps.write()))
42 | .pipe(gulp.dest(`dist/${args.vendor}/styles`))
43 | .pipe(gulpif(args.watch, livereload()))
44 | })
45 |
46 | gulp.task('styles', [
47 | 'styles:css',
48 | 'styles:less',
49 | 'styles:sass'
50 | ])
51 |
--------------------------------------------------------------------------------
/tasks/version.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Bumping version number and tagging the repository with it.
3 | * Please read http://semver.org/
4 | *
5 | * You can use the commands
6 | *
7 | * gulp patch # makes v0.1.0 → v0.1.1
8 | * gulp feature # makes v0.1.1 → v0.2.0
9 | * gulp release # makes v0.2.1 → v1.0.0
10 | *
11 | * To bump the version numbers accordingly after you did a patch,
12 | * introduced a feature or made a backwards-incompatible release.
13 | */
14 |
15 | import gulp from 'gulp'
16 | import git from 'gulp-git'
17 | import bump from 'gulp-bump'
18 | import filter from 'gulp-filter'
19 | import tagVersion from 'gulp-tag-version'
20 |
21 | function inc (importance) {
22 | // get all the files to bump version in
23 | return gulp.src([
24 | 'package.json',
25 | 'app/manifest.json'
26 | ], {
27 | base: './'
28 | })
29 | // bump the version number in those files
30 | .pipe(bump({
31 | type: importance
32 | }))
33 | // save it back to filesystem
34 | .pipe(gulp.dest('./'))
35 | // commit the changed version number
36 | .pipe(git.commit('bump package version'))
37 | // read only one file to get the version number
38 | .pipe(filter('package.json'))
39 | // **tag it in the repository**
40 | .pipe(tagVersion())
41 | }
42 |
43 | gulp.task('patch', () => {
44 | return inc('patch')
45 | })
46 |
47 | gulp.task('feature', () => {
48 | return inc('minor')
49 | })
50 |
51 | gulp.task('release', () => {
52 | return inc('major')
53 | })
54 |
--------------------------------------------------------------------------------