Merge pull request #440 from ueokande/background-clean-architecture
Background clean architecturejh-changes
commit
ed2bd7d75e
74 changed files with 1648 additions and 1344 deletions
@ -1,147 +0,0 @@ |
||||
import actions from '../actions'; |
||||
import * as consoleActions from './console'; |
||||
import * as tabs from '../shared/tabs'; |
||||
import * as bookmarks from '../shared/bookmarks'; |
||||
import * as parsers from 'shared/commands/parsers'; |
||||
import * as properties from 'shared/settings/properties'; |
||||
|
||||
const openCommand = async(url) => { |
||||
let got = await browser.tabs.query({ |
||||
active: true, currentWindow: true |
||||
}); |
||||
if (got.length > 0) { |
||||
return browser.tabs.update(got[0].id, { url: url }); |
||||
} |
||||
}; |
||||
|
||||
const tabopenCommand = (url) => { |
||||
return browser.tabs.create({ url: url }); |
||||
}; |
||||
|
||||
const tabcloseCommand = async() => { |
||||
let got = await browser.tabs.query({ |
||||
active: true, currentWindow: true |
||||
}); |
||||
return browser.tabs.remove(got.map(tab => tab.id)); |
||||
}; |
||||
|
||||
const tabcloseAllCommand = () => { |
||||
return browser.tabs.query({ |
||||
currentWindow: true |
||||
}).then((tabList) => { |
||||
return browser.tabs.remove(tabList.map(tab => tab.id)); |
||||
}); |
||||
}; |
||||
|
||||
const winopenCommand = (url) => { |
||||
return browser.windows.create({ url }); |
||||
}; |
||||
|
||||
const bufferCommand = async(keywords) => { |
||||
if (keywords.length === 0) { |
||||
return; |
||||
} |
||||
let keywordsStr = keywords.join(' '); |
||||
let got = await browser.tabs.query({ |
||||
active: true, currentWindow: true |
||||
}); |
||||
if (got.length === 0) { |
||||
return; |
||||
} |
||||
if (isNaN(keywordsStr)) { |
||||
return tabs.selectByKeyword(got[0], keywordsStr); |
||||
} |
||||
let index = parseInt(keywordsStr, 10) - 1; |
||||
return tabs.selectAt(index); |
||||
}; |
||||
|
||||
const addbookmarkCommand = async(tab, args) => { |
||||
if (!args[0]) { |
||||
return { type: '' }; |
||||
} |
||||
let item = await bookmarks.create(args.join(' '), tab.url); |
||||
if (!item) { |
||||
return consoleActions.error(tab, 'Could not create a bookmark'); |
||||
} |
||||
return consoleActions.info(tab, 'Saved current page: ' + item.url); |
||||
}; |
||||
|
||||
const setCommand = (args) => { |
||||
if (!args[0]) { |
||||
return { type: '' }; |
||||
} |
||||
|
||||
let [name, value] = parsers.parseSetOption(args[0], properties.types); |
||||
return { |
||||
type: actions.SETTING_SET_PROPERTY, |
||||
name, |
||||
value |
||||
}; |
||||
}; |
||||
|
||||
// eslint-disable-next-line complexity, max-lines-per-function
|
||||
const doExec = async(tab, line, settings) => { |
||||
let [name, args] = parsers.parseCommandLine(line); |
||||
|
||||
switch (name) { |
||||
case 'o': |
||||
case 'open': |
||||
await openCommand(parsers.normalizeUrl(args, settings.search)); |
||||
break; |
||||
case 't': |
||||
case 'tabopen': |
||||
await tabopenCommand(parsers.normalizeUrl(args, settings.search)); |
||||
break; |
||||
case 'w': |
||||
case 'winopen': |
||||
await winopenCommand(parsers.normalizeUrl(args, settings.search)); |
||||
break; |
||||
case 'b': |
||||
case 'buffer': |
||||
await bufferCommand(args); |
||||
break; |
||||
case 'bd': |
||||
case 'bdel': |
||||
case 'bdelete': |
||||
await tabs.closeTabByKeywords(args.join(' ')); |
||||
break; |
||||
case 'bd!': |
||||
case 'bdel!': |
||||
case 'bdelete!': |
||||
await tabs.closeTabByKeywordsForce(args.join(' ')); |
||||
break; |
||||
case 'bdeletes': |
||||
await tabs.closeTabsByKeywords(args.join(' ')); |
||||
break; |
||||
case 'bdeletes!': |
||||
await tabs.closeTabsByKeywordsForce(args.join(' ')); |
||||
break; |
||||
case 'addbookmark': |
||||
return addbookmarkCommand(tab, args); |
||||
case 'set': |
||||
return setCommand(args); |
||||
case 'q': |
||||
case 'quit': |
||||
await tabcloseCommand(); |
||||
break; |
||||
case 'qa': |
||||
case 'quitall': |
||||
await tabcloseAllCommand(); |
||||
break; |
||||
default: |
||||
return consoleActions.error(tab, name + ' command is not defined'); |
||||
} |
||||
return { type: '' }; |
||||
}; |
||||
|
||||
// eslint-disable-next-line complexity
|
||||
const exec = async(tab, line, settings) => { |
||||
try { |
||||
let action = await doExec(tab, line, settings); |
||||
return action; |
||||
} catch (e) { |
||||
return consoleActions.error(tab, e.toString()); |
||||
} |
||||
}; |
||||
|
||||
export { exec }; |
@ -1,41 +0,0 @@ |
||||
import messages from 'shared/messages'; |
||||
|
||||
const error = async(tab, text) => { |
||||
await browser.tabs.sendMessage(tab.id, { |
||||
type: messages.CONSOLE_SHOW_ERROR, |
||||
text, |
||||
}); |
||||
return { type: '' }; |
||||
}; |
||||
|
||||
const info = async(tab, text) => { |
||||
await browser.tabs.sendMessage(tab.id, { |
||||
type: messages.CONSOLE_SHOW_INFO, |
||||
text, |
||||
}); |
||||
return { type: '' }; |
||||
}; |
||||
|
||||
const showCommand = async(tab, command) => { |
||||
await browser.tabs.sendMessage(tab.id, { |
||||
type: messages.CONSOLE_SHOW_COMMAND, |
||||
command, |
||||
}); |
||||
return { type: '' }; |
||||
}; |
||||
|
||||
const showFind = async(tab) => { |
||||
await browser.tabs.sendMessage(tab.id, { |
||||
type: messages.CONSOLE_SHOW_FIND |
||||
}); |
||||
return { type: '' }; |
||||
}; |
||||
|
||||
const hide = async(tab) => { |
||||
await browser.tabs.sendMessage(tab.id, { |
||||
type: messages.CONSOLE_HIDE, |
||||
}); |
||||
return { type: '' }; |
||||
}; |
||||
|
||||
export { error, info, showCommand, showFind, hide }; |
@ -1,10 +0,0 @@ |
||||
import actions from './index'; |
||||
|
||||
const setKeyword = (keyword) => { |
||||
return { |
||||
type: actions.FIND_SET_KEYWORD, |
||||
keyword, |
||||
}; |
||||
}; |
||||
|
||||
export { setKeyword }; |
@ -1,11 +0,0 @@ |
||||
export default { |
||||
// Settings
|
||||
SETTING_SET_SETTINGS: 'setting.set.settings', |
||||
SETTING_SET_PROPERTY: 'setting.set.property', |
||||
|
||||
// Find
|
||||
FIND_SET_KEYWORD: 'find.set.keyword', |
||||
|
||||
// Tab
|
||||
TAB_SELECTED: 'tab.selected', |
||||
}; |
@ -1,20 +0,0 @@ |
||||
import actions from '../actions'; |
||||
import * as settingsStorage from 'shared/settings/storage'; |
||||
|
||||
const load = async() => { |
||||
let value = await settingsStorage.loadValue(); |
||||
return { |
||||
type: actions.SETTING_SET_SETTINGS, |
||||
value, |
||||
}; |
||||
}; |
||||
|
||||
const setProperty = (name, value) => { |
||||
return { |
||||
type: actions.SETTING_SET_PROPERTY, |
||||
name, |
||||
value, |
||||
}; |
||||
}; |
||||
|
||||
export { load, setProperty }; |
@ -1,34 +0,0 @@ |
||||
import actions from './index'; |
||||
|
||||
const openNewTab = async( |
||||
url, openerTabId, background = false, adjacent = false |
||||
) => { |
||||
if (!adjacent) { |
||||
await browser.tabs.create({ url, active: !background }); |
||||
return { type: '' }; |
||||
} |
||||
let tabs = await browser.tabs.query({ |
||||
active: true, currentWindow: true |
||||
}); |
||||
await browser.tabs.create({ |
||||
url, |
||||
openerTabId, |
||||
active: !background, |
||||
index: tabs[0].index + 1 |
||||
}); |
||||
return { type: '' }; |
||||
}; |
||||
|
||||
const openToTab = async(url, tab) => { |
||||
await browser.tabs.update(tab.id, { url: url }); |
||||
return { type: '' }; |
||||
}; |
||||
|
||||
const selected = (tabId) => { |
||||
return { |
||||
type: actions.TAB_SELECTED, |
||||
tabId, |
||||
}; |
||||
}; |
||||
|
||||
export { openNewTab, openToTab, selected }; |
@ -1,67 +0,0 @@ |
||||
import messages from 'shared/messages'; |
||||
import * as commandActions from 'background/actions/command'; |
||||
import * as settingActions from 'background/actions/setting'; |
||||
import * as findActions from 'background/actions/find'; |
||||
import * as tabActions from 'background/actions/tab'; |
||||
import * as completions from '../shared/completions'; |
||||
|
||||
export default class BackgroundComponent { |
||||
constructor(store) { |
||||
this.store = store; |
||||
|
||||
browser.runtime.onMessage.addListener((message, sender) => { |
||||
try { |
||||
return this.onMessage(message, sender); |
||||
} catch (e) { |
||||
return browser.tabs.sendMessage(sender.tab.id, { |
||||
type: messages.CONSOLE_SHOW_ERROR, |
||||
text: e.message, |
||||
}); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
onMessage(message, sender) { |
||||
let settings = this.store.getState().setting; |
||||
let find = this.store.getState().find; |
||||
|
||||
switch (message.type) { |
||||
case messages.OPEN_URL: |
||||
if (message.newTab) { |
||||
let action = tabActions.openNewTab( |
||||
message.url, sender.tab.id, message.background, |
||||
settings.value.properties.adjacenttab); |
||||
return this.store.dispatch(action, sender); |
||||
} |
||||
return this.store.dispatch( |
||||
tabActions.openToTab(message.url, sender.tab), sender); |
||||
case messages.CONSOLE_ENTER_COMMAND: |
||||
this.store.dispatch( |
||||
commandActions.exec(sender.tab, message.text, settings.value), |
||||
sender |
||||
); |
||||
return this.broadcastSettingsChanged(); |
||||
case messages.SETTINGS_QUERY: |
||||
return Promise.resolve(this.store.getState().setting.value); |
||||
case messages.CONSOLE_QUERY_COMPLETIONS: |
||||
return completions.complete(message.text, settings.value); |
||||
case messages.SETTINGS_RELOAD: |
||||
this.store.dispatch(settingActions.load()); |
||||
return this.broadcastSettingsChanged(); |
||||
case messages.FIND_GET_KEYWORD: |
||||
return Promise.resolve(find.keyword); |
||||
case messages.FIND_SET_KEYWORD: |
||||
this.store.dispatch(findActions.setKeyword(message.keyword)); |
||||
return Promise.resolve({}); |
||||
} |
||||
} |
||||
|
||||
async broadcastSettingsChanged() { |
||||
let tabs = await browser.tabs.query({}); |
||||
for (let tab of tabs) { |
||||
browser.tabs.sendMessage(tab.id, { |
||||
type: messages.SETTINGS_CHANGED, |
||||
}); |
||||
} |
||||
} |
||||
} |
@ -1,43 +0,0 @@ |
||||
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.browserAction.onClicked.addListener(this.onClicked); |
||||
browser.tabs.onActivated.addListener(async(info) => { |
||||
await browser.tabs.query({ currentWindow: true }); |
||||
return this.onTabActivated(info); |
||||
}); |
||||
} |
||||
|
||||
async onTabActivated(info) { |
||||
let { enabled } = await browser.tabs.sendMessage(info.tabId, { |
||||
type: messages.ADDON_ENABLED_QUERY, |
||||
}); |
||||
return this.updateIndicator(enabled); |
||||
} |
||||
|
||||
onClicked(tab) { |
||||
browser.tabs.sendMessage(tab.id, { |
||||
type: messages.ADDON_TOGGLE_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(); |
||||
} |
||||
} |
@ -1,127 +0,0 @@ |
||||
import messages from 'shared/messages'; |
||||
import operations from 'shared/operations'; |
||||
import * as tabs from '../shared//tabs'; |
||||
import * as zooms from '../shared/zooms'; |
||||
import * as consoleActions from '../actions/console'; |
||||
|
||||
export default class BackgroundComponent { |
||||
constructor(store) { |
||||
this.store = store; |
||||
|
||||
browser.runtime.onMessage.addListener((message, sender) => { |
||||
try { |
||||
return this.onMessage(message, sender); |
||||
} catch (e) { |
||||
return browser.tabs.sendMessage(sender.tab.id, { |
||||
type: messages.CONSOLE_SHOW_ERROR, |
||||
text: e.message, |
||||
}); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
onMessage(message, sender) { |
||||
switch (message.type) { |
||||
case messages.BACKGROUND_OPERATION: |
||||
return this.store.dispatch( |
||||
this.exec(message.operation, sender.tab)); |
||||
} |
||||
} |
||||
|
||||
// eslint-disable-next-line complexity, max-lines-per-function
|
||||
async exec(operation, tab) { |
||||
let tabState = this.store.getState().tab; |
||||
|
||||
switch (operation.type) { |
||||
case operations.TAB_CLOSE: |
||||
await tabs.closeTab(tab.id); |
||||
break; |
||||
case operations.TAB_CLOSE_FORCE: |
||||
await tabs.closeTabForce(tab.id); |
||||
break; |
||||
case operations.TAB_REOPEN: |
||||
await tabs.reopenTab(); |
||||
break; |
||||
case operations.TAB_PREV: |
||||
await tabs.selectPrevTab(tab.index, operation.count); |
||||
break; |
||||
case operations.TAB_NEXT: |
||||
await tabs.selectNextTab(tab.index, operation.count); |
||||
break; |
||||
case operations.TAB_FIRST: |
||||
await tabs.selectFirstTab(); |
||||
break; |
||||
case operations.TAB_LAST: |
||||
await tabs.selectLastTab(); |
||||
break; |
||||
case operations.TAB_PREV_SEL: |
||||
if (tabState.previousSelected > 0) { |
||||
await tabs.selectTab(tabState.previousSelected); |
||||
} |
||||
break; |
||||
case operations.TAB_RELOAD: |
||||
await tabs.reload(tab, operation.cache); |
||||
break; |
||||
case operations.TAB_PIN: |
||||
await tabs.updateTabPinned(tab, true); |
||||
break; |
||||
case operations.TAB_UNPIN: |
||||
await tabs.updateTabPinned(tab, false); |
||||
break; |
||||
case operations.TAB_TOGGLE_PINNED: |
||||
await tabs.toggleTabPinned(tab); |
||||
break; |
||||
case operations.TAB_DUPLICATE: |
||||
await tabs.duplicate(tab.id); |
||||
break; |
||||
case operations.ZOOM_IN: |
||||
await zooms.zoomIn(); |
||||
break; |
||||
case operations.ZOOM_OUT: |
||||
await zooms.zoomOut(); |
||||
break; |
||||
case operations.ZOOM_NEUTRAL: |
||||
await zooms.neutral(); |
||||
break; |
||||
case operations.COMMAND_SHOW: |
||||
return consoleActions.showCommand(tab, ''); |
||||
case operations.COMMAND_SHOW_OPEN: |
||||
if (operation.alter) { |
||||
// alter url
|
||||
return consoleActions.showCommand(tab, 'open ' + tab.url); |
||||
} |
||||
return consoleActions.showCommand(tab, 'open '); |
||||
case operations.COMMAND_SHOW_TABOPEN: |
||||
if (operation.alter) { |
||||
// alter url
|
||||
return consoleActions.showCommand(tab, 'tabopen ' + tab.url); |
||||
} |
||||
return consoleActions.showCommand(tab, 'tabopen '); |
||||
case operations.COMMAND_SHOW_WINOPEN: |
||||
if (operation.alter) { |
||||
// alter url
|
||||
return consoleActions.showCommand(tab, 'winopen ' + tab.url); |
||||
} |
||||
return consoleActions.showCommand(tab, 'winopen '); |
||||
case operations.COMMAND_SHOW_BUFFER: |
||||
return consoleActions.showCommand(tab, 'buffer '); |
||||
case operations.COMMAND_SHOW_ADDBOOKMARK: |
||||
if (operation.alter) { |
||||
return consoleActions.showCommand(tab, 'addbookmark ' + tab.title); |
||||
} |
||||
return consoleActions.showCommand(tab, 'addbookmark '); |
||||
case operations.FIND_START: |
||||
return consoleActions.showFind(tab); |
||||
case operations.CANCEL: |
||||
return consoleActions.hide(tab); |
||||
case operations.PAGE_SOURCE: |
||||
await browser.tabs.create({ |
||||
url: 'view-source:' + tab.url, |
||||
index: tab.index + 1, |
||||
openerTabId: tab.id, |
||||
}); |
||||
break; |
||||
} |
||||
return { type: '' }; |
||||
} |
||||
} |
@ -1,16 +0,0 @@ |
||||
import * as tabActions from '../actions/tab'; |
||||
|
||||
export default class TabComponent { |
||||
constructor(store) { |
||||
this.store = store; |
||||
|
||||
browser.tabs.onActivated.addListener(async(info) => { |
||||
await browser.tabs.query({ currentWindow: true }); |
||||
return this.onTabActivated(info); |
||||
}); |
||||
} |
||||
|
||||
onTabActivated(info) { |
||||
return this.store.dispatch(tabActions.selected(info.tabId)); |
||||
} |
||||
} |
@ -0,0 +1,11 @@ |
||||
import AddonEnabledInteractor from '../usecases/addon-enabled'; |
||||
|
||||
export default class AddonEnabledController { |
||||
constructor() { |
||||
this.addonEnabledInteractor = new AddonEnabledInteractor(); |
||||
} |
||||
|
||||
indicate(enabled) { |
||||
return this.addonEnabledInteractor.indicate(enabled); |
||||
} |
||||
} |
@ -0,0 +1,94 @@ |
||||
import CompletionsInteractor from '../usecases/completions'; |
||||
import CommandInteractor from '../usecases/command'; |
||||
import Completions from '../domains/completions'; |
||||
|
||||
export default class CommandController { |
||||
constructor() { |
||||
this.completionsInteractor = new CompletionsInteractor(); |
||||
this.commandIndicator = new CommandInteractor(); |
||||
} |
||||
|
||||
getCompletions(line) { |
||||
let trimmed = line.trimStart(); |
||||
let words = trimmed.split(/ +/); |
||||
let name = words[0]; |
||||
if (words.length === 1) { |
||||
return this.completionsInteractor.queryConsoleCommand(name); |
||||
} |
||||
let keywords = trimmed.slice(name.length).trimStart(); |
||||
switch (words[0]) { |
||||
case 'o': |
||||
case 'open': |
||||
case 't': |
||||
case 'tabopen': |
||||
case 'w': |
||||
case 'winopen': |
||||
return this.completionsInteractor.queryOpen(name, keywords); |
||||
case 'b': |
||||
case 'buffer': |
||||
return this.completionsInteractor.queryBuffer(name, keywords); |
||||
case 'bd': |
||||
case 'bdel': |
||||
case 'bdelete': |
||||
case 'bdeletes': |
||||
return this.completionsInteractor.queryBdelete(name, keywords); |
||||
case 'bd!': |
||||
case 'bdel!': |
||||
case 'bdelete!': |
||||
case 'bdeletes!': |
||||
return this.completionsInteractor.queryBdeleteForce(name, keywords); |
||||
case 'set': |
||||
return this.completionsInteractor.querySet(name, keywords); |
||||
} |
||||
return Promise.resolve(Completions.empty()); |
||||
} |
||||
|
||||
// eslint-disable-next-line complexity
|
||||
exec(line) { |
||||
let trimmed = line.trimStart(); |
||||
let words = trimmed.split(/ +/); |
||||
let name = words[0]; |
||||
if (words[0].length === 0) { |
||||
return Promise.resolve(); |
||||
} |
||||
|
||||
let keywords = trimmed.slice(name.length).trimStart(); |
||||
switch (words[0]) { |
||||
case 'o': |
||||
case 'open': |
||||
return this.commandIndicator.open(keywords); |
||||
case 't': |
||||
case 'tabopen': |
||||
return this.commandIndicator.tabopen(keywords); |
||||
case 'w': |
||||
case 'winopen': |
||||
return this.commandIndicator.winopen(keywords); |
||||
case 'b': |
||||
case 'buffer': |
||||
return this.commandIndicator.buffer(keywords); |
||||
case 'bd': |
||||
case 'bdel': |
||||
case 'bdelete': |
||||
return this.commandIndicator.bdelete(false, keywords); |
||||
case 'bd!': |
||||
case 'bdel!': |
||||
case 'bdelete!': |
||||
return this.commandIndicator.bdelete(true, keywords); |
||||
case 'bdeletes': |
||||
return this.commandIndicator.bdeletes(false, keywords); |
||||
case 'bdeletes!': |
||||
return this.commandIndicator.bdeletes(true, keywords); |
||||
case 'addbookmark': |
||||
return this.commandIndicator.addbookmark(keywords); |
||||
case 'q': |
||||
case 'quit': |
||||
return this.commandIndicator.quit(); |
||||
case 'qa': |
||||
case 'quitall': |
||||
return this.commandIndicator.quitAll(); |
||||
case 'set': |
||||
return this.commandIndicator.set(keywords); |
||||
} |
||||
throw new Error(words[0] + ' command is not defined'); |
||||
} |
||||
} |
@ -0,0 +1,15 @@ |
||||
import FindInteractor from '../usecases/find'; |
||||
|
||||
export default class FindController { |
||||
constructor() { |
||||
this.findInteractor = new FindInteractor(); |
||||
} |
||||
|
||||
getKeyword() { |
||||
return this.findInteractor.getKeyword(); |
||||
} |
||||
|
||||
setKeyword(keyword) { |
||||
return this.findInteractor.setKeyword(keyword); |
||||
} |
||||
} |
@ -0,0 +1,15 @@ |
||||
import LinkInteractor from '../usecases/link'; |
||||
|
||||
export default class LinkController { |
||||
constructor() { |
||||
this.linkInteractor = new LinkInteractor(); |
||||
} |
||||
|
||||
openToTab(url, tabId) { |
||||
this.linkInteractor.openToTab(url, tabId); |
||||
} |
||||
|
||||
openNewTab(url, openerId, background) { |
||||
this.linkInteractor.openNewTab(url, openerId, background); |
||||
} |
||||
} |
@ -0,0 +1,65 @@ |
||||
import operations from '../../shared/operations'; |
||||
import OperationInteractor from '../usecases/operation'; |
||||
|
||||
export default class OperationController { |
||||
constructor() { |
||||
this.operationInteractor = new OperationInteractor(); |
||||
} |
||||
|
||||
// eslint-disable-next-line complexity, max-lines-per-function
|
||||
exec(operation) { |
||||
switch (operation.type) { |
||||
case operations.TAB_CLOSE: |
||||
return this.operationInteractor.close(false); |
||||
case operations.TAB_CLOSE_FORCE: |
||||
return this.operationInteractor.close(true); |
||||
case operations.TAB_REOPEN: |
||||
return this.operationInteractor.reopen(); |
||||
case operations.TAB_PREV: |
||||
return this.operationInteractor.selectPrev(1); |
||||
case operations.TAB_NEXT: |
||||
return this.operationInteractor.selectNext(1); |
||||
case operations.TAB_FIRST: |
||||
return this.operationInteractor.selectFirst(); |
||||
case operations.TAB_LAST: |
||||
return this.operationInteractor.selectLast(); |
||||
case operations.TAB_PREV_SEL: |
||||
return this.operationInteractor.selectPrevSelected(); |
||||
case operations.TAB_RELOAD: |
||||
return this.operationInteractor.reload(operation.cache); |
||||
case operations.TAB_PIN: |
||||
return this.operationInteractor.setPinned(true); |
||||
case operations.TAB_UNPIN: |
||||
return this.operationInteractor.setPinned(false); |
||||
case operations.TAB_TOGGLE_PINNED: |
||||
return this.operationInteractor.togglePinned(); |
||||
case operations.TAB_DUPLICATE: |
||||
return this.operationInteractor.duplicate(); |
||||
case operations.PAGE_SOURCE: |
||||
return this.operationInteractor.openPageSource(); |
||||
case operations.ZOOM_IN: |
||||
return this.operationInteractor.zoomIn(); |
||||
case operations.ZOOM_OUT: |
||||
return this.operationInteractor.zoomOut(); |
||||
case operations.ZOOM_NEUTRAL: |
||||
return this.operationInteractor.zoomNutoral(); |
||||
case operations.COMMAND_SHOW: |
||||
return this.operationInteractor.showCommand(); |
||||
case operations.COMMAND_SHOW_OPEN: |
||||
return this.operationInteractor.showOpenCommand(operation.alter); |
||||
case operations.COMMAND_SHOW_TABOPEN: |
||||
return this.operationInteractor.showTabopenCommand(operation.alter); |
||||
case operations.COMMAND_SHOW_WINOPEN: |
||||
return this.operationInteractor.showWinopenCommand(operation.alter); |
||||
case operations.COMMAND_SHOW_BUFFER: |
||||
return this.operationInteractor.showBufferCommand(); |
||||
case operations.COMMAND_SHOW_ADDBOOKMARK: |
||||
return this.operationInteractor.showAddbookmarkCommand(operation.alter); |
||||
case operations.FIND_START: |
||||
return this.operationInteractor.findStart(); |
||||
case operations.CANCEL: |
||||
return this.operationInteractor.hideConsole(); |
||||
} |
||||
} |
||||
} |
||||
|
@ -0,0 +1,18 @@ |
||||
import SettingInteractor from '../usecases/setting'; |
||||
import ContentMessageClient from '../infrastructures/content-message-client'; |
||||
|
||||
export default class SettingController { |
||||
constructor() { |
||||
this.settingInteractor = new SettingInteractor(); |
||||
this.contentMessageClient = new ContentMessageClient(); |
||||
} |
||||
|
||||
getSetting() { |
||||
return this.settingInteractor.get(); |
||||
} |
||||
|
||||
async reload() { |
||||
await this.settingInteractor.reload(); |
||||
this.contentMessageClient.broadcastSettingsChanged(); |
||||
} |
||||
} |
@ -0,0 +1,11 @@ |
||||
import VersionInteractor from '../usecases/version'; |
||||
|
||||
export default class VersionController { |
||||
constructor() { |
||||
this.versionInteractor = new VersionInteractor(); |
||||
} |
||||
|
||||
notifyIfUpdated() { |
||||
this.versionInteractor.notifyIfUpdated(); |
||||
} |
||||
} |
@ -0,0 +1,14 @@ |
||||
export default class CompletionGroup { |
||||
constructor(name, items) { |
||||
this.name0 = name; |
||||
this.items0 = items; |
||||
} |
||||
|
||||
get name() { |
||||
return this.name0; |
||||
} |
||||
|
||||
get items() { |
||||
return this.items0; |
||||
} |
||||
} |
@ -0,0 +1,24 @@ |
||||
export default class CompletionItem { |
||||
constructor({ caption, content, url, icon }) { |
||||
this.caption0 = caption; |
||||
this.content0 = content; |
||||
this.url0 = url; |
||||
this.icon0 = icon; |
||||
} |
||||
|
||||
get caption() { |
||||
return this.caption0; |
||||
} |
||||
|
||||
get content() { |
||||
return this.content0; |
||||
} |
||||
|
||||
get url() { |
||||
return this.url0; |
||||
} |
||||
|
||||
get icon() { |
||||
return this.icon0; |
||||
} |
||||
} |
@ -0,0 +1,27 @@ |
||||
export default class Completions { |
||||
constructor(groups) { |
||||
this.g = groups; |
||||
} |
||||
|
||||
get groups() { |
||||
return this.g; |
||||
} |
||||
|
||||
serialize() { |
||||
return this.groups.map(group => ({ |
||||
name: group.name, |
||||
items: group.items.map(item => ({ |
||||
caption: item.caption, |
||||
content: item.content, |
||||
url: item.url, |
||||
icon: item.icon, |
||||
})), |
||||
})); |
||||
} |
||||
|
||||
static EMPTY_COMPLETIONS = new Completions([]); |
||||
|
||||
static empty() { |
||||
return Completions.EMPTY_COMPLETIONS; |
||||
} |
||||
} |
@ -0,0 +1,51 @@ |
||||
import DefaultSettings from '../../shared/settings/default'; |
||||
import * as settingsValues from '../../shared/settings/values'; |
||||
|
||||
export default class Setting { |
||||
constructor({ source, json, form }) { |
||||
this.obj = { |
||||
source, json, form |
||||
}; |
||||
} |
||||
|
||||
get source() { |
||||
return this.obj.source; |
||||
} |
||||
|
||||
get json() { |
||||
return this.obj.json; |
||||
} |
||||
|
||||
get form() { |
||||
return this.obj.form; |
||||
} |
||||
|
||||
value() { |
||||
let value = JSON.parse(DefaultSettings.json); |
||||
if (this.obj.source === 'json') { |
||||
value = settingsValues.valueFromJson(this.obj.json); |
||||
} else if (this.obj.source === 'form') { |
||||
value = settingsValues.valueFromForm(this.obj.form); |
||||
} |
||||
if (!value.properties) { |
||||
value.properties = {}; |
||||
} |
||||
return { ...settingsValues.valueFromJson(DefaultSettings.json), ...value }; |
||||
} |
||||
|
||||
serialize() { |
||||
return this.obj; |
||||
} |
||||
|
||||
static deserialize(obj) { |
||||
return new Setting({ source: obj.source, json: obj.json, form: obj.form }); |
||||
} |
||||
|
||||
static defaultSettings() { |
||||
return new Setting({ |
||||
source: DefaultSettings.source, |
||||
json: DefaultSettings.json, |
||||
form: {}, |
||||
}); |
||||
} |
||||
} |
@ -1,34 +1,8 @@ |
||||
import * as settingActions from 'background/actions/setting'; |
||||
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, applyMiddleware } from 'redux'; |
||||
import promise from 'redux-promise'; |
||||
import * as versions from './shared/versions'; |
||||
import ContentMessageListener from './infrastructures/content-message-listener'; |
||||
import SettingController from './controllers/setting'; |
||||
import VersionController from './controllers/version'; |
||||
|
||||
const store = createStore( |
||||
reducers, |
||||
applyMiddleware(promise), |
||||
); |
||||
new SettingController().reload(); |
||||
new VersionController().notifyIfUpdated(); |
||||
|
||||
const checkAndNotifyUpdated = async() => { |
||||
let updated = await versions.checkUpdated(); |
||||
if (!updated) { |
||||
return; |
||||
} |
||||
await versions.notify(); |
||||
await versions.commit(); |
||||
}; |
||||
|
||||
/* eslint-disable no-unused-vars */ |
||||
const backgroundComponent = new BackgroundComponent(store); |
||||
const operationComponent = new OperationComponent(store); |
||||
const tabComponent = new TabComponent(store); |
||||
const indicatorComponent = new IndicatorComponent(store); |
||||
/* eslint-enable no-unused-vars */ |
||||
|
||||
store.dispatch(settingActions.load()); |
||||
|
||||
checkAndNotifyUpdated(); |
||||
new ContentMessageListener().run(); |
||||
|
@ -0,0 +1,25 @@ |
||||
import messages from '../../shared/messages'; |
||||
|
||||
export default class ContentMessageClient { |
||||
async broadcastSettingsChanged() { |
||||
let tabs = await browser.tabs.query({}); |
||||
for (let tab of tabs) { |
||||
browser.tabs.sendMessage(tab.id, { |
||||
type: messages.SETTINGS_CHANGED, |
||||
}); |
||||
} |
||||
} |
||||
|
||||
async getAddonEnabled(tabId) { |
||||
let { enabled } = await browser.tabs.sendMessage(tabId, { |
||||
type: messages.ADDON_ENABLED_QUERY, |
||||
}); |
||||
return enabled; |
||||
} |
||||
|
||||
toggleAddonEnabled(tabId) { |
||||
return browser.tabs.sendMessage(tabId, { |
||||
type: messages.ADDON_TOGGLE_ENABLED, |
||||
}); |
||||
} |
||||
} |
@ -0,0 +1,101 @@ |
||||
import messages from '../../shared/messages'; |
||||
import CommandController from '../controllers/command'; |
||||
import SettingController from '../controllers/setting'; |
||||
import FindController from '../controllers/find'; |
||||
import AddonEnabledController from '../controllers/addon-enabled'; |
||||
import LinkController from '../controllers/link'; |
||||
import OperationController from '../controllers/operation'; |
||||
|
||||
export default class ContentMessageListener { |
||||
constructor() { |
||||
this.settingController = new SettingController(); |
||||
this.commandController = new CommandController(); |
||||
this.findController = new FindController(); |
||||
this.addonEnabledController = new AddonEnabledController(); |
||||
this.linkController = new LinkController(); |
||||
this.backgroundOperationController = new OperationController(); |
||||
} |
||||
|
||||
run() { |
||||
browser.runtime.onMessage.addListener((message, sender) => { |
||||
try { |
||||
return this.onMessage(message, sender).catch((e) => { |
||||
return browser.tabs.sendMessage(sender.tab.id, { |
||||
type: messages.CONSOLE_SHOW_ERROR, |
||||
text: e.message, |
||||
}); |
||||
}); |
||||
} catch (e) { |
||||
return browser.tabs.sendMessage(sender.tab.id, { |
||||
type: messages.CONSOLE_SHOW_ERROR, |
||||
text: e.message, |
||||
}); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
onMessage(message, sender) { |
||||
switch (message.type) { |
||||
case messages.CONSOLE_QUERY_COMPLETIONS: |
||||
return this.onConsoleQueryCompletions(message.text); |
||||
case messages.CONSOLE_ENTER_COMMAND: |
||||
return this.onConsoleEnterCommand(message.text); |
||||
case messages.SETTINGS_QUERY: |
||||
return this.onSettingsQuery(); |
||||
case messages.SETTINGS_RELOAD: |
||||
return this.onSettingsReload(); |
||||
case messages.FIND_GET_KEYWORD: |
||||
return this.onFindGetKeyword(); |
||||
case messages.FIND_SET_KEYWORD: |
||||
return this.onFindSetKeyword(message.keyword); |
||||
case messages.ADDON_ENABLED_RESPONSE: |
||||
return this.onAddonEnabledResponse(message.enabled); |
||||
case messages.OPEN_URL: |
||||
return this.onOpenUrl( |
||||
message.newTab, message.url, sender.tab.id, message.background); |
||||
case messages.BACKGROUND_OPERATION: |
||||
return this.onBackgroundOperation(message.operation); |
||||
} |
||||
} |
||||
|
||||
async onConsoleQueryCompletions(line) { |
||||
let completions = await this.commandController.getCompletions(line); |
||||
return Promise.resolve(completions.serialize()); |
||||
} |
||||
|
||||
onConsoleEnterCommand(text) { |
||||
return this.commandController.exec(text); |
||||
} |
||||
|
||||
|
||||
onSettingsQuery() { |
||||
return this.settingController.getSetting(); |
||||
} |
||||
|
||||
onSettingsReload() { |
||||
return this.settingController.reload(); |
||||
} |
||||
|
||||
onFindGetKeyword() { |
||||
return this.findController.getKeyword(); |
||||
} |
||||
|
||||
onFindSetKeyword(keyword) { |
||||
return this.findController.setKeyword(keyword); |
||||
} |
||||
|
||||
onAddonEnabledResponse(enabled) { |
||||
return this.addonEnabledController.indicate(enabled); |
||||
} |
||||
|
||||
onOpenUrl(newTab, url, openerId, background) { |
||||
if (newTab) { |
||||
return this.linkController.openNewTab(url, openerId, background); |
||||
} |
||||
return this.linkController.openToTab(url, openerId); |
||||
} |
||||
|
||||
onBackgroundOperation(operation) { |
||||
return this.backgroundOperationController.exec(operation); |
||||
} |
||||
} |
@ -0,0 +1,19 @@ |
||||
const db = {}; |
||||
|
||||
export default class MemoryStorage { |
||||
set(name, value) { |
||||
let data = JSON.stringify(value); |
||||
if (typeof data === 'undefined') { |
||||
throw new Error('value is not serializable'); |
||||
} |
||||
db[name] = data; |
||||
} |
||||
|
||||
get(name) { |
||||
let data = db[name]; |
||||
if (!data) { |
||||
return undefined; |
||||
} |
||||
return JSON.parse(data); |
||||
} |
||||
} |
@ -0,0 +1,23 @@ |
||||
const NOTIFICATION_ID = 'vimvixen-update'; |
||||
|
||||
export default class Notifier { |
||||
notify(title, message, onclick) { |
||||
const listener = (id) => { |
||||
if (id !== NOTIFICATION_ID) { |
||||
return; |
||||
} |
||||
|
||||
onclick(); |
||||
|
||||
browser.notifications.onClicked.removeListener(listener); |
||||
}; |
||||
browser.notifications.onClicked.addListener(listener); |
||||
|
||||
return browser.notifications.create(NOTIFICATION_ID, { |
||||
'type': 'basic', |
||||
'iconUrl': browser.extension.getURL('resources/icon_48x48.png'), |
||||
title, |
||||
message, |
||||
}); |
||||
} |
||||
} |
@ -0,0 +1,36 @@ |
||||
import messages from '../../shared/messages'; |
||||
|
||||
export default class ConsolePresenter { |
||||
showCommand(tabId, command) { |
||||
return browser.tabs.sendMessage(tabId, { |
||||
type: messages.CONSOLE_SHOW_COMMAND, |
||||
command, |
||||
}); |
||||
} |
||||
|
||||
showFind(tabId) { |
||||
return browser.tabs.sendMessage(tabId, { |
||||
type: messages.CONSOLE_SHOW_FIND |
||||
}); |
||||
} |
||||
|
||||
showInfo(tabId, message) { |
||||
return browser.tabs.sendMessage(tabId, { |
||||
type: messages.CONSOLE_SHOW_INFO, |
||||
text: message, |
||||
}); |
||||
} |
||||
|
||||
showError(tabId, message) { |
||||
return browser.tabs.sendMessage(tabId, { |
||||
type: messages.CONSOLE_SHOW_ERROR, |
||||
text: message, |
||||
}); |
||||
} |
||||
|
||||
hide(tabId) { |
||||
return browser.tabs.sendMessage(tabId, { |
||||
type: messages.CONSOLE_HIDE, |
||||
}); |
||||
} |
||||
} |
@ -0,0 +1,12 @@ |
||||
export default class IndicatorPresenter { |
||||
indicate(enabled) { |
||||
let path = enabled |
||||
? 'resources/enabled_32x32.png' |
||||
: 'resources/disabled_32x32.png'; |
||||
return browser.browserAction.setIcon({ path }); |
||||
} |
||||
|
||||
onClick(listener) { |
||||
browser.browserAction.onClicked.addListener(listener); |
||||
} |
||||
} |
@ -0,0 +1,101 @@ |
||||
export default class TabPresenter { |
||||
open(url, tabId) { |
||||
return browser.tabs.update(tabId, { url }); |
||||
} |
||||
|
||||
create(url, opts) { |
||||
return browser.tabs.create({ url, ...opts }); |
||||
} |
||||
|
||||
async getCurrent() { |
||||
let tabs = await browser.tabs.query({ |
||||
active: true, currentWindow: true |
||||
}); |
||||
return tabs[0]; |
||||
} |
||||
|
||||
getAll() { |
||||
return browser.tabs.query({ currentWindow: true }); |
||||
} |
||||
|
||||
async getByKeyword(keyword, excludePinned = false) { |
||||
let tabs = await browser.tabs.query({ currentWindow: true }); |
||||
return tabs.filter((t) => { |
||||
return t.url.toLowerCase().includes(keyword.toLowerCase()) || |
||||
t.title && t.title.toLowerCase().includes(keyword.toLowerCase()); |
||||
}).filter((t) => { |
||||
return !(excludePinned && t.pinned); |
||||
}); |
||||
} |
||||
|
||||
select(tabId) { |
||||
return browser.tabs.update(tabId, { active: true }); |
||||
} |
||||
|
||||
async selectAt(index) { |
||||
let tabs = await browser.tabs.query({ currentWindow: true }); |
||||
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 }); |
||||
} |
||||
|
||||
remove(ids) { |
||||
return browser.tabs.remove(ids); |
||||
} |
||||
|
||||
async reopen() { |
||||
let window = await browser.windows.getCurrent(); |
||||
let sessions = await browser.sessions.getRecentlyClosed(); |
||||
let session = sessions.find((s) => { |
||||
return s.tab && s.tab.windowId === window.id; |
||||
}); |
||||
if (!session) { |
||||
return; |
||||
} |
||||
if (session.tab) { |
||||
return browser.sessions.restore(session.tab.sessionId); |
||||
} |
||||
return browser.sessions.restore(session.window.sessionId); |
||||
} |
||||
|
||||
reload(tabId, cache) { |
||||
return browser.tabs.reload(tabId, { bypassCache: cache }); |
||||
} |
||||
|
||||
setPinned(tabId, pinned) { |
||||
return browser.tabs.update(tabId, { pinned }); |
||||
} |
||||
|
||||
duplicate(id) { |
||||
return browser.tabs.duplicate(id); |
||||
} |
||||
|
||||
getZoom(tabId) { |
||||
return browser.tabs.getZoom(tabId); |
||||
} |
||||
|
||||
setZoom(tabId, factor) { |
||||
return browser.tabs.setZoom(tabId, factor); |
||||
} |
||||
|
||||
async createAdjacent(url, { openerTabId, active }) { |
||||
let tabs = await browser.tabs.query({ |
||||
active: true, currentWindow: true |
||||
}); |
||||
return browser.tabs.create({ |
||||
url, |
||||
openerTabId, |
||||
active, |
||||
index: tabs[0].index + 1 |
||||
}); |
||||
} |
||||
|
||||
onSelected(listener) { |
||||
browser.tabs.onActivated.addListener(listener); |
||||
} |
||||
} |
@ -0,0 +1,5 @@ |
||||
export default class WindowPresenter { |
||||
create(url) { |
||||
return browser.windows.create({ url }); |
||||
} |
||||
} |
@ -1,15 +0,0 @@ |
||||
import actions from 'content/actions'; |
||||
|
||||
const defaultState = { |
||||
keyword: null, |
||||
}; |
||||
|
||||
export default function reducer(state = defaultState, action = {}) { |
||||
switch (action.type) { |
||||
case actions.FIND_SET_KEYWORD: |
||||
return { ...state, |
||||
keyword: action.keyword, }; |
||||
default: |
||||
return state; |
||||
} |
||||
} |
@ -1,8 +0,0 @@ |
||||
import { combineReducers } from 'redux'; |
||||
import setting from './setting'; |
||||
import find from './find'; |
||||
import tab from './tab'; |
||||
|
||||
export default combineReducers({ |
||||
setting, find, tab, |
||||
}); |
@ -1,22 +0,0 @@ |
||||
import actions from 'background/actions'; |
||||
|
||||
const defaultState = { |
||||
value: {}, |
||||
}; |
||||
|
||||
export default function reducer(state = defaultState, action = {}) { |
||||
switch (action.type) { |
||||
case actions.SETTING_SET_SETTINGS: |
||||
return { |
||||
value: action.value, |
||||
}; |
||||
case actions.SETTING_SET_PROPERTY: |
||||
return { |
||||
value: { ...state.value, |
||||
properties: { ...state.value.properties, [action.name]: action.value }} |
||||
}; |
||||
default: |
||||
return state; |
||||
} |
||||
} |
||||
|
@ -1,19 +0,0 @@ |
||||
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; |
||||
} |
||||
} |
||||
|
@ -0,0 +1,13 @@ |
||||
export default class BookmarkRepository { |
||||
async create(title, url) { |
||||
let item = await browser.bookmarks.create({ |
||||
type: 'bookmark', |
||||
title, |
||||
url, |
||||
}); |
||||
if (!item) { |
||||
throw new Error('Could not create a bookmark'); |
||||
} |
||||
return item; |
||||
} |
||||
} |
@ -0,0 +1,31 @@ |
||||
export default class CompletionsRepository { |
||||
async queryBookmarks(keywords) { |
||||
let items = await browser.bookmarks.search({ query: keywords }); |
||||
return items.filter((item) => { |
||||
let url = undefined; |
||||
try { |
||||
url = new URL(item.url); |
||||
} catch (e) { |
||||
return false; |
||||
} |
||||
return item.type === 'bookmark' && url.protocol !== 'place:'; |
||||
}); |
||||
} |
||||
|
||||
queryHistories(keywords) { |
||||
return browser.history.search({ |
||||
text: keywords, |
||||
startTime: 0, |
||||
}); |
||||
} |
||||
|
||||
async queryTabs(keywords, excludePinned) { |
||||
let tabs = await browser.tabs.query({ currentWindow: true }); |
||||
return tabs.filter((t) => { |
||||
return t.url.toLowerCase().includes(keywords.toLowerCase()) || |
||||
t.title && t.title.toLowerCase().includes(keywords.toLowerCase()); |
||||
}).filter((t) => { |
||||
return !(excludePinned && t.pinned); |
||||
}); |
||||
} |
||||
} |
@ -0,0 +1,18 @@ |
||||
import MemoryStorage from '../infrastructures/memory-storage'; |
||||
|
||||
const FIND_KEYWORD_KEY = 'find-keyword'; |
||||
|
||||
export default class FindRepository { |
||||
constructor() { |
||||
this.cache = new MemoryStorage(); |
||||
} |
||||
|
||||
getKeyword() { |
||||
return Promise.resolve(this.cache.get(FIND_KEYWORD_KEY)); |
||||
} |
||||
|
||||
setKeyword(keyword) { |
||||
return this.cache.set(FIND_KEYWORD_KEY, keyword); |
||||
} |
||||
} |
||||
|
@ -0,0 +1,16 @@ |
||||
import Setting from '../domains/setting'; |
||||
|
||||
export default class SettingRepository { |
||||
save(settings) { |
||||
return browser.storage.local.set({ settings: settings.serialize() }); |
||||
} |
||||
|
||||
async load() { |
||||
let { settings } = await browser.storage.local.get('settings'); |
||||
if (!settings) { |
||||
return null; |
||||
} |
||||
return Setting.deserialize(settings); |
||||
} |
||||
} |
||||
|
@ -0,0 +1,23 @@ |
||||
import MemoryStorage from '../infrastructures/memory-storage'; |
||||
|
||||
const CACHED_SETTING_KEY = 'setting'; |
||||
|
||||
export default class SettingRepository { |
||||
constructor() { |
||||
this.cache = new MemoryStorage(); |
||||
} |
||||
|
||||
get() { |
||||
return Promise.resolve(this.cache.get(CACHED_SETTING_KEY)); |
||||
} |
||||
|
||||
update(value) { |
||||
return this.cache.set(CACHED_SETTING_KEY, value); |
||||
} |
||||
|
||||
async setProperty(name, value) { |
||||
let current = await this.get(); |
||||
current.properties[name] = value; |
||||
return this.update(current); |
||||
} |
||||
} |
@ -0,0 +1,10 @@ |
||||
export default class VersionRepository { |
||||
async get() { |
||||
let { version } = await browser.storage.local.get('version'); |
||||
return version; |
||||
} |
||||
|
||||
update(version) { |
||||
return browser.storage.local.set({ version }); |
||||
} |
||||
} |
@ -1,9 +0,0 @@ |
||||
const create = (title, url) => { |
||||
return browser.bookmarks.create({ |
||||
type: 'bookmark', |
||||
title, |
||||
url, |
||||
}); |
||||
}; |
||||
|
||||
export { create }; |
@ -1,14 +0,0 @@ |
||||
const getCompletions = async(keywords) => { |
||||
let items = await browser.bookmarks.search({ query: keywords }); |
||||
return items.filter((item) => { |
||||
let url = undefined; |
||||
try { |
||||
url = new URL(item.url); |
||||
} catch (e) { |
||||
return false; |
||||
} |
||||
return item.type === 'bookmark' && url.protocol !== 'place:'; |
||||
}).slice(0, 10); |
||||
}; |
||||
|
||||
export { getCompletions }; |
@ -1,82 +0,0 @@ |
||||
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 = async(keyword) => { |
||||
let historyItems = await browser.history.search({ |
||||
text: keyword, |
||||
startTime: 0, |
||||
}); |
||||
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 }; |
@ -1,173 +0,0 @@ |
||||
import commandDocs from 'shared/commands/docs'; |
||||
import * as tabs from './tabs'; |
||||
import * as histories from './histories'; |
||||
import * as bookmarks from './bookmarks'; |
||||
import * as properties from 'shared/settings/properties'; |
||||
|
||||
const completeCommands = (typing) => { |
||||
let keys = Object.keys(commandDocs); |
||||
return keys |
||||
.filter(name => name.startsWith(typing)) |
||||
.map(name => ({ |
||||
caption: name, |
||||
content: name, |
||||
url: commandDocs[name], |
||||
})); |
||||
}; |
||||
|
||||
const getSearchCompletions = (command, keywords, searchConfig) => { |
||||
let engineNames = Object.keys(searchConfig.engines); |
||||
let engineItems = engineNames.filter(name => name.startsWith(keywords)) |
||||
.map(name => ({ |
||||
caption: name, |
||||
content: command + ' ' + name |
||||
})); |
||||
return Promise.resolve(engineItems); |
||||
}; |
||||
|
||||
const getHistoryCompletions = async(command, keywords) => { |
||||
let items = await histories.getCompletions(keywords); |
||||
return items.map((page) => { |
||||
return { |
||||
caption: page.title, |
||||
content: command + ' ' + page.url, |
||||
url: page.url |
||||
}; |
||||
}); |
||||
}; |
||||
|
||||
const getBookmarksCompletions = async(command, keywords) => { |
||||
let items = await bookmarks.getCompletions(keywords); |
||||
return items.map(item => ({ |
||||
caption: item.title, |
||||
content: command + ' ' + item.url, |
||||
url: item.url, |
||||
})); |
||||
}; |
||||
|
||||
const getOpenCompletions = async(command, keywords, searchConfig) => { |
||||
let engineItems = await getSearchCompletions(command, keywords, searchConfig); |
||||
let historyItems = await getHistoryCompletions(command, keywords); |
||||
let bookmarkItems = await getBookmarksCompletions(command, keywords); |
||||
let completions = []; |
||||
if (engineItems.length > 0) { |
||||
completions.push({ |
||||
name: 'Search Engines', |
||||
items: engineItems |
||||
}); |
||||
} |
||||
if (historyItems.length > 0) { |
||||
completions.push({ |
||||
name: 'History', |
||||
items: historyItems |
||||
}); |
||||
} |
||||
if (bookmarkItems.length > 0) { |
||||
completions.push({ |
||||
name: 'Bookmarks', |
||||
items: bookmarkItems |
||||
}); |
||||
} |
||||
return completions; |
||||
}; |
||||
|
||||
const getBufferCompletions = async(command, keywords, excludePinned) => { |
||||
let items = await tabs.getCompletions(keywords, excludePinned); |
||||
items = items.map(tab => ({ |
||||
caption: tab.title, |
||||
content: command + ' ' + tab.title, |
||||
url: tab.url, |
||||
icon: tab.favIconUrl |
||||
})); |
||||
return [ |
||||
{ |
||||
name: 'Buffers', |
||||
items: items |
||||
} |
||||
]; |
||||
}; |
||||
|
||||
const getSetCompletions = (command, keywords) => { |
||||
let keys = Object.keys(properties.docs).filter( |
||||
name => name.startsWith(keywords) |
||||
); |
||||
let items = keys.map((key) => { |
||||
if (properties.types[key] === 'boolean') { |
||||
return [ |
||||
{ |
||||
caption: key, |
||||
content: command + ' ' + key, |
||||
url: 'Enable ' + properties.docs[key], |
||||
}, { |
||||
caption: 'no' + key, |
||||
content: command + ' no' + key, |
||||
url: 'Disable ' + properties.docs[key], |
||||
} |
||||
]; |
||||
} |
||||
return [ |
||||
{ |
||||
caption: key, |
||||
content: command + ' ' + key, |
||||
url: 'Set ' + properties.docs[key], |
||||
} |
||||
]; |
||||
}); |
||||
items = items.reduce((acc, val) => acc.concat(val), []); |
||||
if (items.length === 0) { |
||||
return Promise.resolve([]); |
||||
} |
||||
return Promise.resolve([ |
||||
{ |
||||
name: 'Properties', |
||||
items, |
||||
} |
||||
]); |
||||
}; |
||||
|
||||
const complete = (line, settings) => { |
||||
let trimmed = line.trimStart(); |
||||
let words = trimmed.split(/ +/); |
||||
let name = words[0]; |
||||
if (words.length === 1) { |
||||
let items = completeCommands(name); |
||||
if (items.length === 0) { |
||||
return Promise.resolve([]); |
||||
} |
||||
return Promise.resolve([ |
||||
{ |
||||
name: 'Console Command', |
||||
items: completeCommands(name), |
||||
} |
||||
]); |
||||
} |
||||
let keywords = trimmed.slice(name.length).trimStart(); |
||||
|
||||
switch (words[0]) { |
||||
case 'o': |
||||
case 'open': |
||||
case 't': |
||||
case 'tabopen': |
||||
case 'w': |
||||
case 'winopen': |
||||
return getOpenCompletions(name, keywords, settings.search); |
||||
case 'b': |
||||
case 'buffer': |
||||
return getBufferCompletions(name, keywords, false); |
||||
case 'bd!': |
||||
case 'bdel!': |
||||
case 'bdelete!': |
||||
case 'bdeletes!': |
||||
return getBufferCompletions(name, keywords, false); |
||||
case 'bd': |
||||
case 'bdel': |
||||
case 'bdelete': |
||||
case 'bdeletes': |
||||
return getBufferCompletions(name, keywords, true); |
||||
case 'set': |
||||
return getSetCompletions(name, keywords); |
||||
} |
||||
return Promise.resolve([]); |
||||
}; |
||||
|
||||
export { complete }; |
@ -1,8 +0,0 @@ |
||||
import * as tabs from '../tabs'; |
||||
|
||||
const getCompletions = (keyword, excludePinned) => { |
||||
return tabs.queryByKeyword(keyword, excludePinned); |
||||
}; |
||||
|
||||
|
||||
export { getCompletions }; |
@ -1,13 +0,0 @@ |
||||
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 }; |
@ -1,158 +0,0 @@ |
||||
import * as tabCompletions from './completions/tabs'; |
||||
|
||||
const closeTab = async(id) => { |
||||
let tab = await browser.tabs.get(id); |
||||
if (!tab.pinned) { |
||||
return browser.tabs.remove(id); |
||||
} |
||||
}; |
||||
|
||||
const closeTabForce = (id) => { |
||||
return browser.tabs.remove(id); |
||||
}; |
||||
|
||||
const queryByKeyword = async(keyword, excludePinned = false) => { |
||||
let tabs = await browser.tabs.query({ currentWindow: true }); |
||||
return tabs.filter((t) => { |
||||
return t.url.toLowerCase().includes(keyword.toLowerCase()) || |
||||
t.title && t.title.toLowerCase().includes(keyword.toLowerCase()); |
||||
}).filter((t) => { |
||||
return !(excludePinned && t.pinned); |
||||
}); |
||||
}; |
||||
|
||||
const closeTabByKeywords = async(keyword) => { |
||||
let tabs = await queryByKeyword(keyword, true); |
||||
if (tabs.length === 0) { |
||||
throw new Error('No matching buffer for ' + keyword); |
||||
} else if (tabs.length > 1) { |
||||
throw new Error('More than one match for ' + keyword); |
||||
} |
||||
return browser.tabs.remove(tabs[0].id); |
||||
}; |
||||
|
||||
const closeTabByKeywordsForce = async(keyword) => { |
||||
let tabs = await queryByKeyword(keyword, false); |
||||
if (tabs.length === 0) { |
||||
throw new Error('No matching buffer for ' + keyword); |
||||
} else if (tabs.length > 1) { |
||||
throw new Error('More than one match for ' + keyword); |
||||
} |
||||
return browser.tabs.remove(tabs[0].id); |
||||
}; |
||||
|
||||
const closeTabsByKeywords = async(keyword) => { |
||||
let tabs = await tabCompletions.getCompletions(keyword); |
||||
tabs = tabs.filter(tab => !tab.pinned); |
||||
return browser.tabs.remove(tabs.map(tab => tab.id)); |
||||
}; |
||||
|
||||
const closeTabsByKeywordsForce = async(keyword) => { |
||||
let tabs = await tabCompletions.getCompletions(keyword); |
||||
return browser.tabs.remove(tabs.map(tab => tab.id)); |
||||
}; |
||||
|
||||
const reopenTab = async() => { |
||||
let window = await browser.windows.getCurrent(); |
||||
let sessions = await browser.sessions.getRecentlyClosed(); |
||||
let session = sessions.find((s) => { |
||||
return s.tab && s.tab.windowId === window.id; |
||||
}); |
||||
if (!session) { |
||||
return; |
||||
} |
||||
if (session.tab) { |
||||
return browser.sessions.restore(session.tab.sessionId); |
||||
} |
||||
return browser.sessions.restore(session.window.sessionId); |
||||
}; |
||||
|
||||
const selectAt = async(index) => { |
||||
let tabs = await browser.tabs.query({ currentWindow: true }); |
||||
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 = async(current, keyword) => { |
||||
let tabs = await queryByKeyword(keyword); |
||||
if (tabs.length === 0) { |
||||
throw new RangeError('No matching buffer for ' + keyword); |
||||
} |
||||
for (let tab of tabs) { |
||||
if (tab.index > current.index) { |
||||
return browser.tabs.update(tab.id, { active: true }); |
||||
} |
||||
} |
||||
return browser.tabs.update(tabs[0].id, { active: true }); |
||||
}; |
||||
|
||||
const selectPrevTab = async(current, count) => { |
||||
let tabs = await browser.tabs.query({ currentWindow: true }); |
||||
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 = async(current, count) => { |
||||
let tabs = await browser.tabs.query({ currentWindow: true }); |
||||
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 = async() => { |
||||
let tabs = await browser.tabs.query({ currentWindow: true }); |
||||
let id = tabs[0].id; |
||||
return browser.tabs.update(id, { active: true }); |
||||
}; |
||||
|
||||
const selectLastTab = async() => { |
||||
let tabs = await browser.tabs.query({ currentWindow: true }); |
||||
let id = tabs[tabs.length - 1].id; |
||||
return browser.tabs.update(id, { active: true }); |
||||
}; |
||||
|
||||
const selectTab = (id) => { |
||||
return browser.tabs.update(id, { active: true }); |
||||
}; |
||||
|
||||
const reload = (current, cache) => { |
||||
return browser.tabs.reload( |
||||
current.id, |
||||
{ bypassCache: cache } |
||||
); |
||||
}; |
||||
|
||||
const updateTabPinned = (current, pinned) => { |
||||
return browser.tabs.update(current.id, { pinned }); |
||||
}; |
||||
|
||||
const toggleTabPinned = (current) => { |
||||
return updateTabPinned(current, !current.pinned); |
||||
}; |
||||
|
||||
const duplicate = (id) => { |
||||
return browser.tabs.duplicate(id); |
||||
}; |
||||
|
||||
export { |
||||
closeTab, closeTabForce, |
||||
queryByKeyword, closeTabByKeywords, closeTabByKeywordsForce, |
||||
closeTabsByKeywords, closeTabsByKeywordsForce, |
||||
reopenTab, selectAt, selectByKeyword, |
||||
selectPrevTab, selectNextTab, selectFirstTab, |
||||
selectLastTab, selectTab, reload, updateTabPinned, |
||||
toggleTabPinned, duplicate |
||||
}; |
@ -1,38 +0,0 @@ |
||||
import * as storage from './storage'; |
||||
import * as releaseNotes from './release-notes'; |
||||
import manifest from '../../../../manifest.json'; |
||||
|
||||
const NOTIFICATION_ID = 'vimvixen-update'; |
||||
|
||||
const notificationClickListener = (id) => { |
||||
if (id !== NOTIFICATION_ID) { |
||||
return; |
||||
} |
||||
|
||||
browser.tabs.create({ url: releaseNotes.url(manifest.version) }); |
||||
browser.notifications.onClicked.removeListener(notificationClickListener); |
||||
}; |
||||
|
||||
const checkUpdated = async() => { |
||||
let prev = await storage.load(); |
||||
if (!prev) { |
||||
return true; |
||||
} |
||||
return manifest.version !== prev; |
||||
}; |
||||
|
||||
const notify = () => { |
||||
browser.notifications.onClicked.addListener(notificationClickListener); |
||||
return browser.notifications.create(NOTIFICATION_ID, { |
||||
'type': 'basic', |
||||
'iconUrl': browser.extension.getURL('resources/icon_48x48.png'), |
||||
'title': 'Vim Vixen ' + manifest.version + ' has been installed', |
||||
'message': 'Click here to see release notes', |
||||
}); |
||||
}; |
||||
|
||||
const commit = () => { |
||||
storage.save(manifest.version); |
||||
}; |
||||
|
||||
export { checkUpdated, notify, commit }; |
@ -1,8 +0,0 @@ |
||||
const url = (version) => { |
||||
if (version) { |
||||
return 'https://github.com/ueokande/vim-vixen/releases/tag/' + version; |
||||
} |
||||
return 'https://github.com/ueokande/vim-vixen/releases/'; |
||||
}; |
||||
|
||||
export { url }; |
@ -1,10 +0,0 @@ |
||||
const load = async() => { |
||||
let { version } = await browser.storage.local.get('version'); |
||||
return version; |
||||
}; |
||||
|
||||
const save = (version) => { |
||||
return browser.storage.local.set({ version }); |
||||
}; |
||||
|
||||
export { load, save }; |
@ -1,32 +0,0 @@ |
||||
// 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 = async(tabId = undefined) => { |
||||
let current = await browser.tabs.getZoom(tabId); |
||||
let factor = ZOOM_SETTINGS.find(f => f > current); |
||||
if (factor) { |
||||
return browser.tabs.setZoom(tabId, factor); |
||||
} |
||||
}; |
||||
|
||||
const zoomOut = async(tabId = undefined) => { |
||||
let current = await browser.tabs.getZoom(tabId); |
||||
let factor = [].concat(ZOOM_SETTINGS).reverse().find(f => f < current); |
||||
if (factor) { |
||||
return browser.tabs.setZoom(tabId, factor); |
||||
} |
||||
}; |
||||
|
||||
const neutral = (tabId = undefined) => { |
||||
return browser.tabs.setZoom(tabId, 1); |
||||
}; |
||||
|
||||
export { zoomIn, zoomOut, neutral }; |
@ -0,0 +1,29 @@ |
||||
import IndicatorPresenter from '../presenters/indicator'; |
||||
import TabPresenter from '../presenters/tab'; |
||||
import ContentMessageClient from '../infrastructures/content-message-client'; |
||||
|
||||
export default class AddonEnabledInteractor { |
||||
constructor() { |
||||
this.indicatorPresentor = new IndicatorPresenter(); |
||||
|
||||
this.indicatorPresentor.onClick(tab => this.onIndicatorClick(tab.id)); |
||||
|
||||
this.tabPresenter = new TabPresenter(); |
||||
this.tabPresenter.onSelected(info => this.onTabSelected(info.tabId)); |
||||
|
||||
this.contentMessageClient = new ContentMessageClient(); |
||||
} |
||||
|
||||
indicate(enabled) { |
||||
return this.indicatorPresentor.indicate(enabled); |
||||
} |
||||
|
||||
onIndicatorClick(tabId) { |
||||
return this.contentMessageClient.toggleAddonEnabled(tabId); |
||||
} |
||||
|
||||
async onTabSelected(tabId) { |
||||
let enabled = await this.contentMessageClient.getAddonEnabled(tabId); |
||||
return this.indicatorPresentor.indicate(enabled); |
||||
} |
||||
} |
@ -0,0 +1,108 @@ |
||||
import * as parsers from './parsers'; |
||||
import TabPresenter from '../presenters/tab'; |
||||
import WindowPresenter from '../presenters/window'; |
||||
import SettingRepository from '../repositories/setting'; |
||||
import BookmarkRepository from '../repositories/bookmark'; |
||||
import ConsolePresenter from '../presenters/console'; |
||||
import ContentMessageClient from '../infrastructures/content-message-client'; |
||||
import * as properties from 'shared/settings/properties'; |
||||
|
||||
export default class CommandIndicator { |
||||
constructor() { |
||||
this.tabPresenter = new TabPresenter(); |
||||
this.windowPresenter = new WindowPresenter(); |
||||
this.settingRepository = new SettingRepository(); |
||||
this.bookmarkRepository = new BookmarkRepository(); |
||||
this.consolePresenter = new ConsolePresenter(); |
||||
|
||||
this.contentMessageClient = new ContentMessageClient(); |
||||
} |
||||
|
||||
async open(keywords) { |
||||
let url = await this.urlOrSearch(keywords); |
||||
return this.tabPresenter.open(url); |
||||
} |
||||
|
||||
async tabopen(keywords) { |
||||
let url = await this.urlOrSearch(keywords); |
||||
return this.tabPresenter.create(url); |
||||
} |
||||
|
||||
async winopen(keywords) { |
||||
let url = await this.urlOrSearch(keywords); |
||||
return this.windowPresenter.create(url); |
||||
} |
||||
|
||||
async buffer(keywords) { |
||||
if (keywords.length === 0) { |
||||
return; |
||||
} |
||||
if (!isNaN(keywords)) { |
||||
let index = parseInt(keywords, 10) - 1; |
||||
return tabs.selectAt(index); |
||||
} |
||||
|
||||
let current = await this.tabPresenter.getCurrent(); |
||||
let tabs = await this.tabPresenter.getByKeyword(keywords); |
||||
if (tabs.length === 0) { |
||||
throw new RangeError('No matching buffer for ' + keywords); |
||||
} |
||||
for (let tab of tabs) { |
||||
if (tab.index > current.index) { |
||||
return this.tabPresenter.select(tab.id); |
||||
} |
||||
} |
||||
return this.tabPresenter.select(tabs[0].id); |
||||
} |
||||
|
||||
async bdelete(force, keywords) { |
||||
let excludePinned = !force; |
||||
let tabs = await this.tabPresenter.getByKeyword(keywords, excludePinned); |
||||
if (tabs.length === 0) { |
||||
throw new Error('No matching buffer for ' + keywords); |
||||
} else if (tabs.length > 1) { |
||||
throw new Error('More than one match for ' + keywords); |
||||
} |
||||
return this.tabPresenter.remove([tabs[0].id]); |
||||
} |
||||
|
||||
async bdeletes(force, keywords) { |
||||
let excludePinned = !force; |
||||
let tabs = await this.tabPresenter.getByKeyword(keywords, excludePinned); |
||||
let ids = tabs.map(tab => tab.id); |
||||
return this.tabPresenter.remove(ids); |
||||
} |
||||
|
||||
async quit() { |
||||
let tab = await this.tabPresenter.getCurrent(); |
||||
return this.tabPresenter.remove([tab.id]); |
||||
} |
||||
|
||||
async quitAll() { |
||||
let tabs = await this.tabPresenter.getAll(); |
||||
let ids = tabs.map(tab => tab.id); |
||||
this.tabPresenter.remove(ids); |
||||
} |
||||
|
||||
async addbookmark(title) { |
||||
let tab = await this.tabPresenter.getCurrent(); |
||||
let item = await this.bookmarkRepository.create(title, tab.url); |
||||
let message = 'Saved current page: ' + item.url; |
||||
return this.consolePresenter.showInfo(tab.id, message); |
||||
} |
||||
|
||||
async set(keywords) { |
||||
if (keywords.length === 0) { |
||||
return; |
||||
} |
||||
let [name, value] = parsers.parseSetOption(keywords, properties.types); |
||||
await this.settingRepository.setProperty(name, value); |
||||
|
||||
return this.contentMessageClient.broadcastSettingsChanged(); |
||||
} |
||||
|
||||
async urlOrSearch(keywords) { |
||||
let settings = await this.settingRepository.get(); |
||||
return parsers.normalizeUrl(keywords, settings.search); |
||||
} |
||||
} |
@ -0,0 +1,150 @@ |
||||
import CompletionItem from '../domains/completion-item'; |
||||
import CompletionGroup from '../domains/completion-group'; |
||||
import Completions from '../domains/completions'; |
||||
import CommandDocs from '../domains/command-docs'; |
||||
import CompletionRepository from '../repositories/completions'; |
||||
import * as filters from './filters'; |
||||
import SettingRepository from '../repositories/setting'; |
||||
import * as properties from '../../shared/settings/properties'; |
||||
|
||||
const COMPLETION_ITEM_LIMIT = 10; |
||||
|
||||
export default class CompletionsInteractor { |
||||
constructor() { |
||||
this.completionRepository = new CompletionRepository(); |
||||
this.settingRepository = new SettingRepository(); |
||||
} |
||||
|
||||
queryConsoleCommand(prefix) { |
||||
let keys = Object.keys(CommandDocs); |
||||
let items = keys |
||||
.filter(name => name.startsWith(prefix)) |
||||
.map(name => ({ |
||||
caption: name, |
||||
content: name, |
||||
url: CommandDocs[name], |
||||
})); |
||||
|
||||
if (items.length === 0) { |
||||
return Promise.resolve(Completions.empty()); |
||||
} |
||||
return Promise.resolve( |
||||
new Completions([new CompletionGroup('Console Command', items)]) |
||||
); |
||||
} |
||||
|
||||
async queryOpen(name, keywords) { |
||||
let groups = []; |
||||
let engines = await this.querySearchEngineItems(name, keywords); |
||||
if (engines.length > 0) { |
||||
groups.push(new CompletionGroup('Search Engines', engines)); |
||||
} |
||||
let histories = await this.queryHistoryItems(name, keywords); |
||||
if (histories.length > 0) { |
||||
groups.push(new CompletionGroup('History', histories)); |
||||
} |
||||
let bookmarks = await this.queryBookmarkItems(name, keywords); |
||||
if (bookmarks.length > 0) { |
||||
groups.push(new CompletionGroup('Bookmarks', bookmarks)); |
||||
} |
||||
return new Completions(groups); |
||||
} |
||||
|
||||
queryBuffer(name, keywords) { |
||||
return this.queryTabs(name, false, keywords); |
||||
} |
||||
|
||||
queryBdelete(name, keywords) { |
||||
return this.queryTabs(name, true, keywords); |
||||
} |
||||
|
||||
queryBdeleteForce(name, keywords) { |
||||
return this.queryTabs(name, false, keywords); |
||||
} |
||||
|
||||
querySet(name, keywords) { |
||||
let items = Object.keys(properties.docs).map((key) => { |
||||
if (properties.types[key] === 'boolean') { |
||||
return [ |
||||
new CompletionItem({ |
||||
caption: key, |
||||
content: name + ' ' + key, |
||||
url: 'Enable ' + properties.docs[key], |
||||
}), |
||||
new CompletionItem({ |
||||
caption: 'no' + key, |
||||
content: name + ' no' + key, |
||||
url: 'Disable ' + properties.docs[key], |
||||
}), |
||||
]; |
||||
} |
||||
return [ |
||||
new CompletionItem({ |
||||
caption: key, |
||||
content: name + ' ' + key, |
||||
url: 'Set ' + properties.docs[key], |
||||
}) |
||||
]; |
||||
}); |
||||
items = items.reduce((acc, val) => acc.concat(val), []); |
||||
items = items.filter((item) => { |
||||
return item.caption.startsWith(keywords); |
||||
}); |
||||
if (items.length === 0) { |
||||
return Promise.resolve(Completions.empty()); |
||||
} |
||||
return Promise.resolve( |
||||
new Completions([new CompletionGroup('Properties', items)]) |
||||
); |
||||
} |
||||
|
||||
async queryTabs(name, excludePinned, args) { |
||||
let tabs = await this.completionRepository.queryTabs(args, excludePinned); |
||||
let items = tabs.map(tab => new CompletionItem({ |
||||
caption: tab.title, |
||||
content: name + ' ' + tab.title, |
||||
url: tab.url, |
||||
icon: tab.favIconUrl |
||||
})); |
||||
if (items.length === 0) { |
||||
return Promise.resolve(Completions.empty()); |
||||
} |
||||
return new Completions([new CompletionGroup('Buffers', items)]); |
||||
} |
||||
|
||||
async querySearchEngineItems(name, keywords) { |
||||
let settings = await this.settingRepository.get(); |
||||
let engines = Object.keys(settings.search.engines) |
||||
.filter(key => key.startsWith(keywords)); |
||||
return engines.map(key => new CompletionItem({ |
||||
caption: key, |
||||
content: name + ' ' + key, |
||||
})); |
||||
} |
||||
|
||||
async queryHistoryItems(name, keywords) { |
||||
let histories = await this.completionRepository.queryHistories(keywords); |
||||
histories = [histories] |
||||
.map(filters.filterBlankTitle) |
||||
.map(filters.filterHttp) |
||||
.map(filters.filterByTailingSlash) |
||||
.map(pages => filters.filterByPathname(pages, COMPLETION_ITEM_LIMIT)) |
||||
.map(pages => filters.filterByOrigin(pages, COMPLETION_ITEM_LIMIT))[0] |
||||
.sort((x, y) => x.visitCount < y.visitCount) |
||||
.slice(0, COMPLETION_ITEM_LIMIT); |
||||
return histories.map(page => new CompletionItem({ |
||||
caption: page.title, |
||||
content: name + ' ' + page.url, |
||||
url: page.url |
||||
})); |
||||
} |
||||
|
||||
async queryBookmarkItems(name, keywords) { |
||||
let bookmarks = await this.completionRepository.queryBookmarks(keywords); |
||||
return bookmarks.map(page => new CompletionItem({ |
||||
caption: page.title, |
||||
content: name + ' ' + page.url, |
||||
url: page.url |
||||
})); |
||||
} |
||||
} |
@ -0,0 +1,72 @@ |
||||
const filterHttp = (items) => { |
||||
let httpsHosts = items.map(x => new URL(x.url)) |
||||
.filter(x => x.protocol === 'https:') |
||||
.map(x => x.host); |
||||
httpsHosts = new Set(httpsHosts); |
||||
|
||||
return items.filter((item) => { |
||||
let url = new URL(item.url); |
||||
return url.protocol === 'https:' || !httpsHosts.has(url.host); |
||||
}); |
||||
}; |
||||
|
||||
const filterBlankTitle = (items) => { |
||||
return items.filter(item => item.title && item.title !== ''); |
||||
}; |
||||
|
||||
const filterByTailingSlash = (items) => { |
||||
let urls = items.map(item => new URL(item.url)); |
||||
let simplePaths = urls |
||||
.filter(url => url.hash === '' && url.search === '') |
||||
.map(url => url.origin + url.pathname); |
||||
simplePaths = new Set(simplePaths); |
||||
|
||||
return items.filter((item) => { |
||||
let url = new URL(item.url); |
||||
if (url.hash !== '' || url.search !== '' || |
||||
url.pathname.slice(-1) !== '/') { |
||||
return true; |
||||
} |
||||
return !simplePaths.has(url.origin + url.pathname.slice(0, -1)); |
||||
}); |
||||
}; |
||||
|
||||
const filterByPathname = (items, min) => { |
||||
let hash = {}; |
||||
for (let item of items) { |
||||
let url = new URL(item.url); |
||||
let pathname = url.origin + url.pathname; |
||||
if (!hash[pathname]) { |
||||
hash[pathname] = item; |
||||
} else if (hash[pathname].url.length > item.url.length) { |
||||
hash[pathname] = item; |
||||
} |
||||
} |
||||
let filtered = Object.values(hash); |
||||
if (filtered.length < min) { |
||||
return items; |
||||
} |
||||
return filtered; |
||||
}; |
||||
|
||||
const filterByOrigin = (items, min) => { |
||||
let hash = {}; |
||||
for (let item of items) { |
||||
let origin = new URL(item.url).origin; |
||||
if (!hash[origin]) { |
||||
hash[origin] = item; |
||||
} else if (hash[origin].url.length > item.url.length) { |
||||
hash[origin] = item; |
||||
} |
||||
} |
||||
let filtered = Object.values(hash); |
||||
if (filtered.length < min) { |
||||
return items; |
||||
} |
||||
return filtered; |
||||
}; |
||||
|
||||
export { |
||||
filterHttp, filterBlankTitle, filterByTailingSlash, |
||||
filterByPathname, filterByOrigin |
||||
}; |
@ -0,0 +1,15 @@ |
||||
import FindRepository from '../repositories/find'; |
||||
|
||||
export default class FindInteractor { |
||||
constructor() { |
||||
this.findRepository = new FindRepository(); |
||||
} |
||||
|
||||
getKeyword() { |
||||
return this.findRepository.getKeyword(); |
||||
} |
||||
|
||||
setKeyword(keyword) { |
||||
return this.findRepository.setKeyword(keyword); |
||||
} |
||||
} |
@ -0,0 +1,27 @@ |
||||
import SettingRepository from '../repositories/setting'; |
||||
import TabPresenter from '../presenters/tab'; |
||||
|
||||
export default class LinkInteractor { |
||||
constructor() { |
||||
this.settingRepository = new SettingRepository(); |
||||
this.tabPresenter = new TabPresenter(); |
||||
} |
||||
|
||||
openToTab(url, tabId) { |
||||
this.tabPresenter.open(url, tabId); |
||||
|
||||
} |
||||
|
||||
async openNewTab(url, openerId, background) { |
||||
let settings = await this.settingRepository.get(); |
||||
let { adjacenttab } = settings.properties; |
||||
if (adjacenttab) { |
||||
return this.tabPresenter.create(url, { |
||||
openerTabId: openerId, active: !background |
||||
}); |
||||
} |
||||
return this.tabPresenter.create(url, { |
||||
openerTabId: openerId, active: !background |
||||
}); |
||||
} |
||||
} |
@ -0,0 +1,192 @@ |
||||
import MemoryStorage from '../infrastructures/memory-storage'; |
||||
import TabPresenter from '../presenters/tab'; |
||||
import ConsolePresenter from '../presenters/console'; |
||||
|
||||
const CURRENT_SELECTED_KEY = 'tabs.current.selected'; |
||||
const LAST_SELECTED_KEY = 'tabs.last.selected'; |
||||
|
||||
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 |
||||
]; |
||||
|
||||
export default class OperationInteractor { |
||||
constructor() { |
||||
this.tabPresenter = new TabPresenter(); |
||||
this.tabPresenter.onSelected(info => this.onTabSelected(info.tabId)); |
||||
|
||||
this.consolePresenter = new ConsolePresenter(); |
||||
|
||||
this.cache = new MemoryStorage(); |
||||
} |
||||
|
||||
async close(force) { |
||||
let tab = await this.tabPresenter.getCurrent(); |
||||
if (!force && tab.pinned) { |
||||
return; |
||||
} |
||||
return this.tabPresenter.remove([tab.id]); |
||||
} |
||||
|
||||
reopen() { |
||||
return this.tabPresenter.reopen(); |
||||
} |
||||
|
||||
async selectPrev(count) { |
||||
let tabs = await this.tabPresenter.getAll(); |
||||
if (tabs.length < 2) { |
||||
return; |
||||
} |
||||
let tab = tabs.find(t => t.active); |
||||
if (!tab) { |
||||
return; |
||||
} |
||||
let select = (tab.index - count + tabs.length) % tabs.length; |
||||
return this.tabPresenter.select(tabs[select].id); |
||||
} |
||||
|
||||
async selectNext(count) { |
||||
let tabs = await this.tabPresenter.getAll(); |
||||
if (tabs.length < 2) { |
||||
return; |
||||
} |
||||
let tab = tabs.find(t => t.active); |
||||
if (!tab) { |
||||
return; |
||||
} |
||||
let select = (tab.index + count) % tabs.length; |
||||
return this.tabPresenter.select(tabs[select].id); |
||||
} |
||||
|
||||
async selectFirst() { |
||||
let tabs = await this.tabPresenter.getAll(); |
||||
return this.tabPresenter.select(tabs[0].id); |
||||
} |
||||
|
||||
async selectLast() { |
||||
let tabs = await this.tabPresenter.getAll(); |
||||
return this.tabPresenter.select(tabs[tabs.length - 1].id); |
||||
} |
||||
|
||||
async selectPrevSelected() { |
||||
let tabId = await this.cache.get(LAST_SELECTED_KEY); |
||||
if (tabId === null || typeof tabId === 'undefined') { |
||||
return; |
||||
} |
||||
this.tabPresenter.select(tabId); |
||||
} |
||||
|
||||
async reload(cache) { |
||||
let tab = await this.tabPresenter.getCurrent(); |
||||
return this.tabPresenter.reload(tab.id, cache); |
||||
} |
||||
|
||||
async setPinned(pinned) { |
||||
let tab = await this.tabPresenter.getCurrent(); |
||||
return this.tabPresenter.setPinned(tab.id, pinned); |
||||
} |
||||
|
||||
async togglePinned() { |
||||
let tab = await this.tabPresenter.getCurrent(); |
||||
return this.tabPresenter.setPinned(tab.id, !tab.pinned); |
||||
} |
||||
|
||||
async duplicate() { |
||||
let tab = await this.tabPresenter.getCurrent(); |
||||
return this.tabPresenter.duplicate(tab.id); |
||||
} |
||||
|
||||
async openPageSource() { |
||||
let tab = await this.tabPresenter.getCurrent(); |
||||
let url = 'view-source:' + tab.url; |
||||
return this.tabPresenter.create(url); |
||||
} |
||||
|
||||
async zoomIn(tabId) { |
||||
let tab = await this.tabPresenter.getCurrent(); |
||||
let current = await this.tabPresenter.getZoom(tab.id); |
||||
let factor = ZOOM_SETTINGS.find(f => f > current); |
||||
if (factor) { |
||||
return this.tabPresenter.setZoom(tabId, factor); |
||||
} |
||||
} |
||||
|
||||
async zoomOut(tabId) { |
||||
let tab = await this.tabPresenter.getCurrent(); |
||||
let current = await this.tabPresenter.getZoom(tab.id); |
||||
let factor = [].concat(ZOOM_SETTINGS).reverse().find(f => f < current); |
||||
if (factor) { |
||||
return this.tabPresenter.setZoom(tabId, factor); |
||||
} |
||||
} |
||||
|
||||
zoomNutoral(tabId) { |
||||
return this.tabPresenter.setZoom(tabId, 1); |
||||
} |
||||
|
||||
async showCommand() { |
||||
let tab = await this.tabPresenter.getCurrent(); |
||||
this.consolePresenter.showCommand(tab.id, ''); |
||||
} |
||||
|
||||
async showOpenCommand(alter) { |
||||
let tab = await this.tabPresenter.getCurrent(); |
||||
let command = 'open '; |
||||
if (alter) { |
||||
command += tab.url; |
||||
} |
||||
return this.consolePresenter.showCommand(tab.id, command); |
||||
} |
||||
|
||||
async showTabopenCommand(alter) { |
||||
let tab = await this.tabPresenter.getCurrent(); |
||||
let command = 'tabopen '; |
||||
if (alter) { |
||||
command += tab.url; |
||||
} |
||||
return this.consolePresenter.showCommand(tab.id, command); |
||||
} |
||||
|
||||
async showWinopenCommand(alter) { |
||||
let tab = await this.tabPresenter.getCurrent(); |
||||
let command = 'winopen '; |
||||
if (alter) { |
||||
command += tab.url; |
||||
} |
||||
return this.consolePresenter.showCommand(tab.id, command); |
||||
} |
||||
|
||||
async showBufferCommand() { |
||||
let tab = await this.tabPresenter.getCurrent(); |
||||
let command = 'buffer '; |
||||
return this.consolePresenter.showCommand(tab.id, command); |
||||
} |
||||
|
||||
async showAddbookmarkCommand(alter) { |
||||
let tab = await this.tabPresenter.getCurrent(); |
||||
let command = 'addbookmark '; |
||||
if (alter) { |
||||
command += tab.title; |
||||
} |
||||
return this.consolePresenter.showCommand(tab.id, command); |
||||
} |
||||
|
||||
async findStart() { |
||||
let tab = await this.tabPresenter.getCurrent(); |
||||
this.consolePresenter.showFind(tab.id); |
||||
} |
||||
|
||||
async hideConsole() { |
||||
let tab = await this.tabPresenter.getCurrent(); |
||||
this.consolePresenter.hide(tab.id); |
||||
} |
||||
|
||||
onTabSelected(tabId) { |
||||
let lastId = this.cache.get(CURRENT_SELECTED_KEY); |
||||
if (lastId) { |
||||
this.cache.set(LAST_SELECTED_KEY, lastId); |
||||
} |
||||
this.cache.set(CURRENT_SELECTED_KEY, tabId); |
||||
} |
||||
} |
||||
|
@ -0,0 +1,31 @@ |
||||
import Setting from '../domains/setting'; |
||||
import PersistentSettingRepository from '../repositories/persistent-setting'; |
||||
import SettingRepository from '../repositories/setting'; |
||||
|
||||
export default class SettingInteractor { |
||||
constructor() { |
||||
this.persistentSettingRepository = new PersistentSettingRepository(); |
||||
this.settingRepository = new SettingRepository(); |
||||
} |
||||
|
||||
save(settings) { |
||||
this.persistentSettingRepository.save(settings); |
||||
} |
||||
|
||||
get() { |
||||
return this.settingRepository.get(); |
||||
} |
||||
|
||||
async reload() { |
||||
let settings = await this.persistentSettingRepository.load(); |
||||
if (!settings) { |
||||
settings = Setting.defaultSettings(); |
||||
} |
||||
|
||||
let value = settings.value(); |
||||
|
||||
this.settingRepository.update(value); |
||||
|
||||
return value; |
||||
} |
||||
} |
@ -0,0 +1,41 @@ |
||||
import manifest from '../../../manifest.json'; |
||||
import VersionRepository from '../repositories/version'; |
||||
import TabPresenter from '../presenters/tab'; |
||||
import Notifier from '../infrastructures/notifier'; |
||||
|
||||
export default class VersionInteractor { |
||||
constructor() { |
||||
this.versionRepository = new VersionRepository(); |
||||
this.tabPresenter = new TabPresenter(); |
||||
this.notifier = new Notifier(); |
||||
} |
||||
|
||||
async notifyIfUpdated() { |
||||
if (!await this.checkUpdated()) { |
||||
return; |
||||
} |
||||
|
||||
let title = 'Vim Vixen ' + manifest.version + ' has been installed'; |
||||
let message = 'Click here to see release notes'; |
||||
this.notifier.notify(title, message, () => { |
||||
let url = this.releaseNoteUrl(manifest.version); |
||||
this.tabPresenter.create(url); |
||||
}); |
||||
this.versionRepository.update(manifest.version); |
||||
} |
||||
|
||||
async checkUpdated() { |
||||
let prev = await this.versionRepository.get(); |
||||
if (!prev) { |
||||
return true; |
||||
} |
||||
return manifest.version !== prev; |
||||
} |
||||
|
||||
releaseNoteUrl(version) { |
||||
if (version) { |
||||
return 'https://github.com/ueokande/vim-vixen/releases/tag/' + version; |
||||
} |
||||
return 'https://github.com/ueokande/vim-vixen/releases/'; |
||||
} |
||||
} |
@ -1,12 +0,0 @@ |
||||
import actions from 'background/actions'; |
||||
import * as findActions from 'background/actions/find'; |
||||
|
||||
describe("find actions", () => { |
||||
describe("setKeyword", () => { |
||||
it('create FIND_SET_KEYWORD action', () => { |
||||
let action = findActions.setKeyword('banana'); |
||||
expect(action.type).to.equal(actions.FIND_SET_KEYWORD); |
||||
expect(action.keyword).to.equal('banana'); |
||||
}); |
||||
}); |
||||
}); |
@ -1,13 +0,0 @@ |
||||
import actions from 'background/actions'; |
||||
import * as tabActions from 'background/actions/tab'; |
||||
|
||||
describe("tab actions", () => { |
||||
describe("selected", () => { |
||||
it('create TAB_SELECTED action', () => { |
||||
let action = tabActions.selected(123); |
||||
expect(action.type).to.equal(actions.TAB_SELECTED); |
||||
expect(action.tabId).to.equal(123); |
||||
}); |
||||
}); |
||||
}); |
||||
|
@ -0,0 +1,46 @@ |
||||
import MemoryStorage from 'background/infrastructures/memory-storage'; |
||||
|
||||
describe("background/infrastructures/memory-storage", () => { |
||||
let versionRepository; |
||||
|
||||
it('stores values', () => { |
||||
let cache = new MemoryStorage(); |
||||
cache.set('number', 123); |
||||
expect(cache.get('number')).to.equal(123); |
||||
|
||||
cache.set('string', '123'); |
||||
expect(cache.get('string')).to.equal('123'); |
||||
|
||||
cache.set('object', { hello: '123' }); |
||||
expect(cache.get('object')).to.deep.equal({ hello: '123' }); |
||||
}); |
||||
|
||||
it('returns undefined if no keys', () => { |
||||
let cache = new MemoryStorage(); |
||||
expect(cache.get('no-keys')).to.be.undefined; |
||||
}) |
||||
|
||||
it('stored on shared memory', () => { |
||||
let cache = new MemoryStorage(); |
||||
cache.set('red', 'apple'); |
||||
|
||||
cache = new MemoryStorage(); |
||||
let got = cache.get('red'); |
||||
expect(got).to.equal('apple'); |
||||
}); |
||||
|
||||
it('stored cloned objects', () => { |
||||
let cache = new MemoryStorage(); |
||||
let recipe = { sugar: '300g' }; |
||||
cache.set('recipe', recipe); |
||||
|
||||
recipe.salt = '20g' |
||||
let got = cache.get('recipe', recipe); |
||||
expect(got).to.deep.equal({ sugar: '300g' }); |
||||
}); |
||||
|
||||
it('throws an error with unserializable objects', () => { |
||||
let cache = new MemoryStorage(); |
||||
expect(() => cache.set('fn', setTimeout)).to.throw(); |
||||
}) |
||||
}); |
@ -1,18 +0,0 @@ |
||||
import actions from 'background/actions'; |
||||
import findReducer from 'background/reducers/find'; |
||||
|
||||
describe("find reducer", () => { |
||||
it('return the initial state', () => { |
||||
let state = findReducer(undefined, {}); |
||||
expect(state).to.have.deep.property('keyword', null); |
||||
}); |
||||
|
||||
it('return next state for FIND_SET_KEYWORD', () => { |
||||
let action = { |
||||
type: actions.FIND_SET_KEYWORD, |
||||
keyword: 'cherry', |
||||
}; |
||||
let state = findReducer(undefined, action); |
||||
expect(state).to.have.deep.property('keyword', 'cherry') |
||||
}); |
||||
}); |
@ -1,35 +0,0 @@ |
||||
import actions from 'background/actions'; |
||||
import settingReducer from 'background/reducers/setting'; |
||||
|
||||
describe("setting reducer", () => { |
||||
it('return the initial state', () => { |
||||
let state = settingReducer(undefined, {}); |
||||
expect(state).to.have.deep.property('value', {}); |
||||
}); |
||||
|
||||
it('return next state for SETTING_SET_SETTINGS', () => { |
||||
let action = { |
||||
type: actions.SETTING_SET_SETTINGS, |
||||
value: { key: 123 }, |
||||
}; |
||||
let state = settingReducer(undefined, action); |
||||
expect(state).to.have.deep.property('value', { key: 123 }); |
||||
}); |
||||
|
||||
it('return next state for SETTING_SET_PROPERTY', () => { |
||||
let state = { |
||||
value: { |
||||
properties: { smoothscroll: true } |
||||
} |
||||
} |
||||
let action = { |
||||
type: actions.SETTING_SET_PROPERTY, |
||||
name: 'encoding', |
||||
value: 'utf-8', |
||||
}; |
||||
state = settingReducer(state, action); |
||||
|
||||
expect(state.value.properties).to.have.property('smoothscroll', true); |
||||
expect(state.value.properties).to.have.property('encoding', 'utf-8'); |
||||
}); |
||||
}); |
@ -1,22 +0,0 @@ |
||||
import actions from 'background/actions'; |
||||
import tabReducer from 'background/reducers/tab'; |
||||
|
||||
describe("tab reducer", () => { |
||||
it('return the initial state', () => { |
||||
let state = tabReducer(undefined, {}); |
||||
expect(state.previousSelected).to.equal(-1); |
||||
expect(state.currentSelected).to.equal(-1); |
||||
}); |
||||
|
||||
it('return next state for TAB_SELECTED', () => { |
||||
let state = undefined; |
||||
|
||||
state = tabReducer(state, { type: actions.TAB_SELECTED, tabId: 123 }); |
||||
expect(state.previousSelected).to.equal(-1); |
||||
expect(state.currentSelected).to.equal(123); |
||||
|
||||
state = tabReducer(state, { type: actions.TAB_SELECTED, tabId: 456 }); |
||||
expect(state.previousSelected).to.equal(123); |
||||
expect(state.currentSelected).to.equal(456); |
||||
}); |
||||
}); |
@ -1,40 +0,0 @@ |
||||
import * as versions from 'background/shared/versions'; |
||||
import manifest from '../../../../manifest.json'; |
||||
|
||||
describe("shared/versions/storage", () => { |
||||
describe('#checkUpdated', () => { |
||||
beforeEach(() => { |
||||
return browser.storage.local.remove('version'); |
||||
}); |
||||
|
||||
it('return true if no previous versions', async() => { |
||||
let updated = await versions.checkUpdated(); |
||||
expect(updated).to.be.true; |
||||
}); |
||||
|
||||
it('return true if updated', async() => { |
||||
await browser.storage.local.set({ version: '0.001' }); |
||||
let updated = await versions.checkUpdated(); |
||||
expect(updated).to.be.true; |
||||
}); |
||||
|
||||
it('return false if not updated', async() => { |
||||
await browser.storage.local.set({ version: manifest.version }); |
||||
let updated = await versions.checkUpdated(); |
||||
expect(updated).to.be.false; |
||||
}); |
||||
}); |
||||
|
||||
describe('#commit', () => { |
||||
beforeEach(() => { |
||||
return browser.storage.local.remove('version'); |
||||
}); |
||||
|
||||
it('saves current version from manifest.json', async() => { |
||||
await versions.commit(); |
||||
let { version } = await browser.storage.local.get('version'); |
||||
expect(version).to.be.a('string'); |
||||
expect(version).to.equal(manifest.version); |
||||
}); |
||||
}); |
||||
}); |
@ -0,0 +1,113 @@ |
||||
import * as filters from 'background/usecases/filters'; |
||||
|
||||
describe("background/usecases/filters", () => { |
||||
describe('filterHttp', () => { |
||||
it('filters http URLs duplicates to https hosts', () => { |
||||
let pages = [ |
||||
{ url: 'http://i-beam.org/foo' }, |
||||
{ url: 'https://i-beam.org/bar' }, |
||||
{ url: 'http://i-beam.net/hoge' }, |
||||
{ url: 'http://i-beam.net/fuga' }, |
||||
]; |
||||
let filtered = filters.filterHttp(pages); |
||||
|
||||
let urls = filtered.map(x => x.url); |
||||
expect(urls).to.deep.equal([ |
||||
'https://i-beam.org/bar', 'http://i-beam.net/hoge', 'http://i-beam.net/fuga' |
||||
]); |
||||
}) |
||||
}); |
||||
|
||||
describe('filterBlankTitle', () => { |
||||
it('filters blank titles', () => { |
||||
let pages = [ |
||||
{ title: 'hello' }, |
||||
{ title: '' }, |
||||
{}, |
||||
]; |
||||
let filtered = filters.filterBlankTitle(pages); |
||||
|
||||
expect(filtered).to.deep.equal([{ title: 'hello' }]); |
||||
}); |
||||
}) |
||||
|
||||
describe('filterByTailingSlash', () => { |
||||
it('filters duplicated pathname on tailing slash', () => { |
||||
let pages = [ |
||||
{ url: 'http://i-beam.org/content' }, |
||||
{ url: 'http://i-beam.org/content/' }, |
||||
{ url: 'http://i-beam.org/search' }, |
||||
{ url: 'http://i-beam.org/search?q=apple_banana_cherry' }, |
||||
]; |
||||
let filtered = filters.filterByTailingSlash(pages); |
||||
|
||||
let urls = filtered.map(x => x.url); |
||||
expect(urls).to.deep.equal([ |
||||
'http://i-beam.org/content', |
||||
'http://i-beam.org/search', |
||||
'http://i-beam.org/search?q=apple_banana_cherry', |
||||
]); |
||||
}); |
||||
}) |
||||
|
||||
describe('filterByPathname', () => { |
||||
it('remains items less than minimam length', () => { |
||||
let pages = [ |
||||
{ url: 'http://i-beam.org/search?q=apple' }, |
||||
{ url: 'http://i-beam.org/search?q=apple_banana' }, |
||||
{ url: 'http://i-beam.org/search?q=apple_banana_cherry' }, |
||||
{ url: 'http://i-beam.org/request?q=apple' }, |
||||
{ url: 'http://i-beam.org/request?q=apple_banana' }, |
||||
{ url: 'http://i-beam.org/request?q=apple_banana_cherry' }, |
||||
]; |
||||
let filtered = filters.filterByPathname(pages, 10); |
||||
expect(filtered).to.have.lengthOf(6); |
||||
}); |
||||
|
||||
it('filters by length of pathname', () => { |
||||
let pages = [ |
||||
{ url: 'http://i-beam.org/search?q=apple' }, |
||||
{ url: 'http://i-beam.org/search?q=apple_banana' }, |
||||
{ url: 'http://i-beam.org/search?q=apple_banana_cherry' }, |
||||
{ url: 'http://i-beam.net/search?q=apple' }, |
||||
{ url: 'http://i-beam.net/search?q=apple_banana' }, |
||||
{ url: 'http://i-beam.net/search?q=apple_banana_cherry' }, |
||||
]; |
||||
let filtered = filters.filterByPathname(pages, 0); |
||||
expect(filtered).to.deep.equal([ |
||||
{ url: 'http://i-beam.org/search?q=apple' }, |
||||
{ url: 'http://i-beam.net/search?q=apple' }, |
||||
]); |
||||
}); |
||||
}) |
||||
|
||||
describe('filterByOrigin', () => { |
||||
it('remains items less than minimam length', () => { |
||||
let pages = [ |
||||
{ url: 'http://i-beam.org/search?q=apple' }, |
||||
{ url: 'http://i-beam.org/search?q=apple_banana' }, |
||||
{ url: 'http://i-beam.org/search?q=apple_banana_cherry' }, |
||||
{ url: 'http://i-beam.org/request?q=apple' }, |
||||
{ url: 'http://i-beam.org/request?q=apple_banana' }, |
||||
{ url: 'http://i-beam.org/request?q=apple_banana_cherry' }, |
||||
]; |
||||
let filtered = filters.filterByOrigin(pages, 10); |
||||
expect(filtered).to.have.lengthOf(6); |
||||
}); |
||||
|
||||
it('filters by length of pathname', () => { |
||||
let pages = [ |
||||
{ url: 'http://i-beam.org/search?q=apple' }, |
||||
{ url: 'http://i-beam.org/search?q=apple_banana' }, |
||||
{ url: 'http://i-beam.org/search?q=apple_banana_cherry' }, |
||||
{ url: 'http://i-beam.org/request?q=apple' }, |
||||
{ url: 'http://i-beam.org/request?q=apple_banana' }, |
||||
{ url: 'http://i-beam.org/request?q=apple_banana_cherry' }, |
||||
]; |
||||
let filtered = filters.filterByOrigin(pages, 0); |
||||
expect(filtered).to.deep.equal([ |
||||
{ url: 'http://i-beam.org/search?q=apple' }, |
||||
]); |
||||
}); |
||||
}) |
||||
}); |
Reference in new issue