From 98bc2326eeeb5d915706dee9aadc2ac3e9af1789 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sun, 6 May 2018 11:45:07 +0900 Subject: Refactor background directories --- src/background/shared/completions/histories.js | 83 +++++++++++++++ src/background/shared/completions/index.js | 84 +++++++++++++++ src/background/shared/completions/tabs.js | 10 ++ src/background/shared/tabs.js | 136 +++++++++++++++++++++++++ src/background/shared/zooms.js | 38 +++++++ 5 files changed, 351 insertions(+) create mode 100644 src/background/shared/completions/histories.js create mode 100644 src/background/shared/completions/index.js create mode 100644 src/background/shared/completions/tabs.js create mode 100644 src/background/shared/tabs.js create mode 100644 src/background/shared/zooms.js (limited to 'src/background/shared') diff --git a/src/background/shared/completions/histories.js b/src/background/shared/completions/histories.js new file mode 100644 index 0000000..a7d3d47 --- /dev/null +++ b/src/background/shared/completions/histories.js @@ -0,0 +1,83 @@ +const filterHttp = (items) => { + const httpsHosts = items + .filter(item => item[1].protocol === 'https:') + .map(item => item[1].host); + const httpsHostSet = new Set(httpsHosts); + return items.filter( + item => !(item[1].protocol === 'http:' && httpsHostSet.has(item[1].host)) + ); +}; + +const filterEmptyTitle = (items) => { + return items.filter(item => item[0].title && item[0].title !== ''); +}; + +const filterClosedPath = (items) => { + const allSimplePaths = items + .filter(item => item[1].hash === '' && item[1].search === '') + .map(item => item[1].origin + item[1].pathname); + const allSimplePathSet = new Set(allSimplePaths); + return items.filter( + item => !(item[1].hash === '' && item[1].search === '' && + (/\/$/).test(item[1].pathname) && + allSimplePathSet.has( + (item[1].origin + item[1].pathname).replace(/\/$/, '') + ) + ) + ); +}; + +const reduceByPathname = (items, min) => { + let hash = {}; + for (let item of items) { + let pathname = item[1].origin + item[1].pathname; + if (!hash[pathname]) { + hash[pathname] = item; + } else if (hash[pathname][1].href.length > item[1].href.length) { + hash[pathname] = item; + } + } + let filtered = Object.values(hash); + if (filtered.length < min) { + return items; + } + return filtered; +}; + +const reduceByOrigin = (items, min) => { + let hash = {}; + for (let item of items) { + let origin = item[1].origin; + if (!hash[origin]) { + hash[origin] = item; + } else if (hash[origin][1].href.length > item[1].href.length) { + hash[origin] = item; + } + } + let filtered = Object.values(hash); + if (filtered.length < min) { + return items; + } + return filtered; +}; + +const getCompletions = (keyword) => { + return browser.history.search({ + text: keyword, + startTime: 0, + }).then((historyItems) => { + return [historyItems.map(item => [item, new URL(item.url)])] + .map(filterEmptyTitle) + .map(filterHttp) + .map(filterClosedPath) + .map(items => reduceByPathname(items, 10)) + .map(items => reduceByOrigin(items, 10)) + .map(items => items + .sort((x, y) => x[0].visitCount < y[0].visitCount) + .slice(0, 10) + .map(item => item[0]) + )[0]; + }); +}; + +export { getCompletions }; diff --git a/src/background/shared/completions/index.js b/src/background/shared/completions/index.js new file mode 100644 index 0000000..73b7b27 --- /dev/null +++ b/src/background/shared/completions/index.js @@ -0,0 +1,84 @@ +import * as tabs from './tabs'; +import * as histories from './histories'; + +const getOpenCompletions = (command, keywords, searchConfig) => { + return histories.getCompletions(keywords).then((pages) => { + let historyItems = pages.map((page) => { + return { + caption: page.title, + content: command + ' ' + page.url, + url: page.url + }; + }); + let engineNames = Object.keys(searchConfig.engines); + let engineItems = engineNames.filter(name => name.startsWith(keywords)) + .map(name => ({ + caption: name, + content: command + ' ' + name + })); + + let completions = []; + if (engineItems.length > 0) { + completions.push({ + name: 'Search Engines', + items: engineItems + }); + } + if (historyItems.length > 0) { + completions.push({ + name: 'History', + items: historyItems + }); + } + return completions; + }); +}; + +const getCompletions = (line, settings) => { + let typedWords = line.trim().split(/ +/); + let typing = ''; + if (!line.endsWith(' ')) { + typing = typedWords.pop(); + } + + if (typedWords.length === 0) { + return Promise.resolve([]); + } + let name = typedWords.shift(); + let keywords = typedWords.concat(typing).join(' '); + + switch (name) { + case 'o': + case 'open': + case 't': + case 'tabopen': + case 'w': + case 'winopen': + return getOpenCompletions(name, keywords, settings.search); + case 'b': + case 'buffer': + return tabs.getCompletions(keywords).then((gotTabs) => { + let items = gotTabs.map((tab) => { + return { + caption: tab.title, + content: name + ' ' + tab.title, + url: tab.url, + icon: tab.favIconUrl + }; + }); + return [ + { + name: 'Buffers', + items: items + } + ]; + }); + } + return Promise.resolve([]); +}; + +const complete = (line, settings) => { + return getCompletions(line, settings); +}; + +export { complete }; diff --git a/src/background/shared/completions/tabs.js b/src/background/shared/completions/tabs.js new file mode 100644 index 0000000..5edddca --- /dev/null +++ b/src/background/shared/completions/tabs.js @@ -0,0 +1,10 @@ +const getCompletions = (keyword) => { + return browser.tabs.query({ currentWindow: true }).then((tabs) => { + let matched = tabs.filter((t) => { + return t.url.includes(keyword) || t.title && t.title.includes(keyword); + }); + return matched; + }); +}; + +export { getCompletions }; diff --git a/src/background/shared/tabs.js b/src/background/shared/tabs.js new file mode 100644 index 0000000..277afb2 --- /dev/null +++ b/src/background/shared/tabs.js @@ -0,0 +1,136 @@ +let prevSelTab = 1; +let currSelTab = 1; + +browser.tabs.onActivated.addListener((activeInfo) => { + return browser.tabs.query({ currentWindow: true }).then(() => { + prevSelTab = currSelTab; + currSelTab = activeInfo.tabId; + }); +}); + +const closeTab = (id) => { + return browser.tabs.get(id).then((tab) => { + if (!tab.pinned) { + return browser.tabs.remove(id); + } + }); +}; + +const closeTabForce = (id) => { + return browser.tabs.remove(id); +}; + +const reopenTab = () => { + return browser.sessions.getRecentlyClosed({ + maxResults: 1 + }).then((sessions) => { + if (sessions.length === 0) { + return; + } + let session = sessions[0]; + if (session.tab) { + return browser.sessions.restore(session.tab.sessionId); + } + return browser.sessions.restore(session.window.sessionId); + }); +}; + +const selectAt = (index) => { + return browser.tabs.query({ currentWindow: true }).then((tabs) => { + if (tabs.length < 2) { + return; + } + if (index < 0 || tabs.length <= index) { + throw new RangeError(`tab ${index + 1} does not exist`); + } + let id = tabs[index].id; + return browser.tabs.update(id, { active: true }); + }); +}; + +const selectByKeyword = (current, keyword) => { + return browser.tabs.query({ currentWindow: true }).then((tabs) => { + let matched = tabs.filter((t) => { + return t.url.includes(keyword) || t.title.includes(keyword); + }); + + if (matched.length === 0) { + throw new RangeError('No matching buffer for ' + keyword); + } + for (let tab of matched) { + if (tab.index > current.index) { + return browser.tabs.update(tab.id, { active: true }); + } + } + return browser.tabs.update(matched[0].id, { active: true }); + }); +}; + +const selectPrevTab = (current, count) => { + return browser.tabs.query({ currentWindow: true }).then((tabs) => { + if (tabs.length < 2) { + return; + } + let select = (current - count + tabs.length) % tabs.length; + let id = tabs[select].id; + return browser.tabs.update(id, { active: true }); + }); +}; + +const selectNextTab = (current, count) => { + return browser.tabs.query({ currentWindow: true }).then((tabs) => { + if (tabs.length < 2) { + return; + } + let select = (current + count) % tabs.length; + let id = tabs[select].id; + return browser.tabs.update(id, { active: true }); + }); +}; + +const selectFirstTab = () => { + return browser.tabs.query({ currentWindow: true }).then((tabs) => { + let id = tabs[0].id; + return browser.tabs.update(id, { active: true }); + }); +}; + +const selectLastTab = () => { + return browser.tabs.query({ currentWindow: true }).then((tabs) => { + let id = tabs[tabs.length - 1].id; + return browser.tabs.update(id, { active: true }); + }); +}; + +const selectPrevSelTab = () => { + return browser.tabs.update(prevSelTab, { active: true }); +}; + +const reload = (current, cache) => { + return browser.tabs.reload( + current.id, + { bypassCache: cache } + ); +}; + +const updateTabPinned = (current, pinned) => { + return browser.tabs.query({ currentWindow: true, active: true }) + .then(() => { + return browser.tabs.update(current.id, { pinned: pinned }); + }); +}; + +const toggleTabPinned = (current) => { + updateTabPinned(current, !current.pinned); +}; + +const duplicate = (id) => { + return browser.tabs.duplicate(id); +}; + +export { + closeTab, closeTabForce, reopenTab, selectAt, selectByKeyword, + selectPrevTab, selectNextTab, selectFirstTab, + selectLastTab, selectPrevSelTab, reload, updateTabPinned, + toggleTabPinned, duplicate +}; diff --git a/src/background/shared/zooms.js b/src/background/shared/zooms.js new file mode 100644 index 0000000..e3e2aa6 --- /dev/null +++ b/src/background/shared/zooms.js @@ -0,0 +1,38 @@ +// For chromium +// const ZOOM_SETTINGS = [ +// 0.25, 0.33, 0.50, 0.66, 0.75, 0.80, 0.90, 1.00, +// 1.10, 1.25, 1.50, 1.75, 2.00, 2.50, 3.00, 4.00, 5.00 +// ]; + +const ZOOM_SETTINGS = [ + 0.33, 0.50, 0.66, 0.75, 0.80, 0.90, 1.00, + 1.10, 1.25, 1.50, 1.75, 2.00, 2.50, 3.00 +]; + +const zoomIn = (tabId = undefined) => { + return browser.tabs.getZoom(tabId).then((factor) => { + for (let f of ZOOM_SETTINGS) { + if (f > factor) { + browser.tabs.setZoom(tabId, f); + break; + } + } + }); +}; + +const zoomOut = (tabId = undefined) => { + return browser.tabs.getZoom(tabId).then((factor) => { + for (let f of [].concat(ZOOM_SETTINGS).reverse()) { + if (f < factor) { + browser.tabs.setZoom(tabId, f); + break; + } + } + }); +}; + +const neutral = (tabId = undefined) => { + return browser.tabs.setZoom(tabId, 1); +}; + +export { zoomIn, zoomOut, neutral }; -- cgit v1.2.3 From 2c366ac3b1d7114869567845c955238e96056565 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sun, 6 May 2018 16:11:40 +0900 Subject: Previous selected tab as redux --- src/background/actions/index.js | 3 +++ src/background/actions/tab.js | 11 ++++++++++- src/background/components/operation.js | 7 ++++++- src/background/components/tab.js | 17 +++++++++++++++++ src/background/index.js | 3 +++ src/background/reducers/index.js | 3 +++ src/background/reducers/tab.js | 19 +++++++++++++++++++ src/background/shared/tabs.js | 16 +++------------- 8 files changed, 64 insertions(+), 15 deletions(-) create mode 100644 src/background/components/tab.js create mode 100644 src/background/reducers/tab.js (limited to 'src/background/shared') diff --git a/src/background/actions/index.js b/src/background/actions/index.js index 2bdaaf2..3833389 100644 --- a/src/background/actions/index.js +++ b/src/background/actions/index.js @@ -5,4 +5,7 @@ export default { // Find FIND_SET_KEYWORD: 'find.set.keyword', + + // Tab + TAB_SELECTED: 'tab.selected', }; diff --git a/src/background/actions/tab.js b/src/background/actions/tab.js index 3c642fd..0d439fd 100644 --- a/src/background/actions/tab.js +++ b/src/background/actions/tab.js @@ -1,3 +1,5 @@ +import actions from './index'; + const openNewTab = (url, openerTabId, background = false, adjacent = false) => { if (adjacent) { return browser.tabs.query({ @@ -18,4 +20,11 @@ const openToTab = (url, tab) => { return browser.tabs.update(tab.id, { url: url }); }; -export { openNewTab, openToTab }; +const selected = (tabId) => { + return { + type: actions.TAB_SELECTED, + tabId, + }; +}; + +export { openNewTab, openToTab, selected }; diff --git a/src/background/components/operation.js b/src/background/components/operation.js index b9581c9..9a0b4e1 100644 --- a/src/background/components/operation.js +++ b/src/background/components/operation.js @@ -30,6 +30,8 @@ export default class BackgroundComponent { // eslint-disable-next-line complexity exec(operation, tab) { + let tabState = this.store.getState().tab; + switch (operation.type) { case operations.TAB_CLOSE: return tabs.closeTab(tab.id); @@ -46,7 +48,10 @@ export default class BackgroundComponent { case operations.TAB_LAST: return tabs.selectLastTab(); case operations.TAB_PREV_SEL: - return tabs.selectPrevSelTab(); + if (tabState.previousSelected > 0) { + return tabs.selectTab(tabState.previousSelected); + } + break; case operations.TAB_RELOAD: return tabs.reload(tab, operation.cache); case operations.TAB_PIN: diff --git a/src/background/components/tab.js b/src/background/components/tab.js new file mode 100644 index 0000000..b273546 --- /dev/null +++ b/src/background/components/tab.js @@ -0,0 +1,17 @@ +import * as tabActions from '../actions/tab'; + +export default class TabComponent { + constructor(store) { + this.store = store; + + browser.tabs.onActivated.addListener((info) => { + return browser.tabs.query({ currentWindow: true }).then(() => { + return this.onTabActivated(info); + }); + }); + } + + onTabActivated(info) { + return this.store.dispatch(tabActions.selected(info.tabId)); + } +} diff --git a/src/background/index.js b/src/background/index.js index 4f6b23f..be042ce 100644 --- a/src/background/index.js +++ b/src/background/index.js @@ -2,6 +2,7 @@ import * as settingActions from 'background/actions/setting'; import messages from 'shared/messages'; import BackgroundComponent from 'background/components/background'; import OperationComponent from 'background/components/operation'; +import TabComponent from 'background/components/tab'; import reducers from 'background/reducers'; import { createStore } from 'shared/store'; import * as versions from 'shared/versions'; @@ -19,6 +20,8 @@ const store = createStore(reducers, (e, sender) => { const backgroundComponent = new BackgroundComponent(store); // eslint-disable-next-line no-unused-vars const operationComponent = new OperationComponent(store); +// eslint-disable-next-line no-unused-vars +const tabComponent = new TabComponent(store); store.dispatch(settingActions.load()); diff --git a/src/background/reducers/index.js b/src/background/reducers/index.js index 63ff0f8..5729f0a 100644 --- a/src/background/reducers/index.js +++ b/src/background/reducers/index.js @@ -1,15 +1,18 @@ import settingReducer from './setting'; import findReducer from './find'; +import tabReducer from './tab'; // Make setting reducer instead of re-use const defaultState = { setting: settingReducer(undefined, {}), find: findReducer(undefined, {}), + tab: tabReducer(undefined, {}), }; export default function reducer(state = defaultState, action = {}) { return Object.assign({}, state, { setting: settingReducer(state.setting, action), find: findReducer(state.find, action), + tab: tabReducer(state.tab, action), }); } diff --git a/src/background/reducers/tab.js b/src/background/reducers/tab.js new file mode 100644 index 0000000..e0cdf32 --- /dev/null +++ b/src/background/reducers/tab.js @@ -0,0 +1,19 @@ +import actions from 'background/actions'; + +const defaultState = { + previousSelected: -1, + currentSelected: -1, +}; + +export default function reducer(state = defaultState, action = {}) { + switch (action.type) { + case actions.TAB_SELECTED: + return { + previousSelected: state.currentSelected, + currentSelected: action.tabId, + }; + default: + return state; + } +} + diff --git a/src/background/shared/tabs.js b/src/background/shared/tabs.js index 277afb2..f1dcc73 100644 --- a/src/background/shared/tabs.js +++ b/src/background/shared/tabs.js @@ -1,13 +1,3 @@ -let prevSelTab = 1; -let currSelTab = 1; - -browser.tabs.onActivated.addListener((activeInfo) => { - return browser.tabs.query({ currentWindow: true }).then(() => { - prevSelTab = currSelTab; - currSelTab = activeInfo.tabId; - }); -}); - const closeTab = (id) => { return browser.tabs.get(id).then((tab) => { if (!tab.pinned) { @@ -102,8 +92,8 @@ const selectLastTab = () => { }); }; -const selectPrevSelTab = () => { - return browser.tabs.update(prevSelTab, { active: true }); +const selectTab = (id) => { + return browser.tabs.update(id, { active: true }); }; const reload = (current, cache) => { @@ -131,6 +121,6 @@ const duplicate = (id) => { export { closeTab, closeTabForce, reopenTab, selectAt, selectByKeyword, selectPrevTab, selectNextTab, selectFirstTab, - selectLastTab, selectPrevSelTab, reload, updateTabPinned, + selectLastTab, selectTab, reload, updateTabPinned, toggleTabPinned, duplicate }; -- cgit v1.2.3 From 129aae38df1a17a5850898d5832bb8112ead3cbb Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sun, 6 May 2018 22:30:04 +0900 Subject: Indicator shows the add-on enabled --- manifest.json | 6 +++++ resources/disabled_32x32.png | Bin 0 -> 1426 bytes resources/enabled_32x32.png | Bin 0 -> 1504 bytes src/background/components/indicator.js | 38 ++++++++++++++++++++++++++++ src/background/index.js | 8 +++--- src/background/shared/indicators.js | 13 ++++++++++ src/content/components/common/index.js | 14 ++++++++++ src/content/components/top-content/index.js | 7 +++++ src/shared/messages.js | 3 +++ 9 files changed, 86 insertions(+), 3 deletions(-) create mode 100644 resources/disabled_32x32.png create mode 100644 resources/enabled_32x32.png create mode 100644 src/background/components/indicator.js create mode 100644 src/background/shared/indicators.js (limited to 'src/background/shared') diff --git a/manifest.json b/manifest.json index 4c43b64..39d14f4 100644 --- a/manifest.json +++ b/manifest.json @@ -40,5 +40,11 @@ ], "options_ui": { "page": "build/settings.html" + }, + "browser_action": { + "default_icon": { + "32": "resources/enabled_32x32.png" + }, + "default_title": "Vim Vixen" } } diff --git a/resources/disabled_32x32.png b/resources/disabled_32x32.png new file mode 100644 index 0000000..d3b88db Binary files /dev/null and b/resources/disabled_32x32.png differ diff --git a/resources/enabled_32x32.png b/resources/enabled_32x32.png new file mode 100644 index 0000000..eaf2862 Binary files /dev/null and b/resources/enabled_32x32.png differ diff --git a/src/background/components/indicator.js b/src/background/components/indicator.js new file mode 100644 index 0000000..ccdcc74 --- /dev/null +++ b/src/background/components/indicator.js @@ -0,0 +1,38 @@ +import * as indicators from '../shared/indicators'; +import messages from 'shared/messages'; + +export default class IndicatorComponent { + constructor(store) { + this.store = store; + + messages.onMessage(this.onMessage.bind(this)); + + browser.tabs.onActivated.addListener((info) => { + return browser.tabs.query({ currentWindow: true }).then(() => { + return this.onTabActivated(info); + }); + }); + } + + onTabActivated(info) { + return browser.tabs.sendMessage(info.tabId, { + type: messages.ADDON_ENABLED_QUERY, + }).then((resp) => { + return this.updateIndicator(resp.enabled); + }); + } + + onMessage(message) { + switch (message.type) { + case messages.ADDON_ENABLED_RESPONSE: + return this.updateIndicator(message.enabled); + } + } + + updateIndicator(enabled) { + if (enabled) { + return indicators.enable(); + } + return indicators.disable(); + } +} diff --git a/src/background/index.js b/src/background/index.js index be042ce..3f1013c 100644 --- a/src/background/index.js +++ b/src/background/index.js @@ -3,6 +3,7 @@ import messages from 'shared/messages'; import BackgroundComponent from 'background/components/background'; import OperationComponent from 'background/components/operation'; import TabComponent from 'background/components/tab'; +import IndicatorComponent from 'background/components/indicator'; import reducers from 'background/reducers'; import { createStore } from 'shared/store'; import * as versions from 'shared/versions'; @@ -16,12 +17,13 @@ const store = createStore(reducers, (e, sender) => { }); } }); -// eslint-disable-next-line no-unused-vars + +/* eslint-disable no-unused-vars */ const backgroundComponent = new BackgroundComponent(store); -// eslint-disable-next-line no-unused-vars const operationComponent = new OperationComponent(store); -// eslint-disable-next-line no-unused-vars const tabComponent = new TabComponent(store); +const indicatorComponent = new IndicatorComponent(store); +/* eslint-enable no-unused-vars */ store.dispatch(settingActions.load()); diff --git a/src/background/shared/indicators.js b/src/background/shared/indicators.js new file mode 100644 index 0000000..74002c4 --- /dev/null +++ b/src/background/shared/indicators.js @@ -0,0 +1,13 @@ +const enable = () => { + return browser.browserAction.setIcon({ + path: 'resources/enabled_32x32.png', + }); +}; + +const disable = () => { + return browser.browserAction.setIcon({ + path: 'resources/disabled_32x32.png', + }); +}; + +export { enable, disable }; diff --git a/src/content/components/common/index.js b/src/content/components/common/index.js index 565632c..c5d1df7 100644 --- a/src/content/components/common/index.js +++ b/src/content/components/common/index.js @@ -14,10 +14,12 @@ export default class Common { input.onKey(key => keymapper.key(key)); this.store = store; + this.prevEnabled = this.store.getState().addon.enabled; this.reloadSettings(); messages.onMessage(this.onMessage.bind(this)); + store.subscribe(() => this.update()); } onMessage(message) { @@ -27,6 +29,18 @@ export default class Common { } } + update() { + let enabled = this.store.getState().addon.enabled; + if (enabled !== this.prevEnabled) { + this.prevEnabled = enabled; + + browser.runtime.sendMessage({ + type: messages.ADDON_ENABLED_RESPONSE, + enabled, + }); + } + } + reloadSettings() { browser.runtime.sendMessage({ type: messages.SETTINGS_QUERY, diff --git a/src/content/components/top-content/index.js b/src/content/components/top-content/index.js index cf21ec4..cb9e160 100644 --- a/src/content/components/top-content/index.js +++ b/src/content/components/top-content/index.js @@ -48,11 +48,18 @@ export default class TopContent { } onMessage(message) { + let addonState = this.store.getState().addon; + switch (message.type) { case messages.CONSOLE_UNFOCUS: this.win.focus(); consoleFrames.blur(window.document); return Promise.resolve(); + case messages.ADDON_ENABLED_QUERY: + return Promise.resolve({ + type: messages.ADDON_ENABLED_RESPONSE, + enabled: addonState.enabled, + }); } } } diff --git a/src/shared/messages.js b/src/shared/messages.js index a404658..2a286bf 100644 --- a/src/shared/messages.js +++ b/src/shared/messages.js @@ -48,6 +48,9 @@ export default { FIND_GET_KEYWORD: 'find.get.keyword', FIND_SET_KEYWORD: 'find.set.keyword', + ADDON_ENABLED_QUERY: 'addon.enabled.query', + ADDON_ENABLED_RESPONSE: 'addon.enabled.response', + OPEN_URL: 'open.url', SETTINGS_RELOAD: 'settings.reload', -- cgit v1.2.3