Merge pull request #440 from ueokande/background-clean-architecture
Background clean architecture
This commit is contained in:
		
						commit
						ed2bd7d75e
					
				
					 74 changed files with 1648 additions and 1344 deletions
				
			
		|  | @ -165,6 +165,7 @@ describe("tab test", () => { | |||
|   }); | ||||
| 
 | ||||
|   it('opens view-source by gf', async () => { | ||||
|     await new Promise(resolve => setTimeout(resolve, 100)); | ||||
|     let win = await windows.get(targetWindow.id); | ||||
|     let tab = win.tabs[0]; | ||||
|     await keys.press(tab.id, 'g'); | ||||
|  |  | |||
|  | @ -13,9 +13,9 @@ module.exports = function (config) { | |||
|     ], | ||||
| 
 | ||||
|     preprocessors: { | ||||
|       'test/main.js': [ 'webpack' ], | ||||
|       'test/**/*.test.js': [ 'webpack' ], | ||||
|       'test/**/*.test.jsx': [ 'webpack' ], | ||||
|       'test/main.js': [ 'webpack', 'sourcemap' ], | ||||
|       'test/**/*.test.js': [ 'webpack', 'sourcemap' ], | ||||
|       'test/**/*.test.jsx': [ 'webpack', 'sourcemap' ], | ||||
|       'test/**/*.html': ['html2js'] | ||||
|     }, | ||||
| 
 | ||||
|  |  | |||
|  | @ -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)); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										11
									
								
								src/background/controllers/addon-enabled.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/background/controllers/addon-enabled.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -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); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										94
									
								
								src/background/controllers/command.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								src/background/controllers/command.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -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'); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										15
									
								
								src/background/controllers/find.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/background/controllers/find.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -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); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										15
									
								
								src/background/controllers/link.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/background/controllers/link.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -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); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										65
									
								
								src/background/controllers/operation.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								src/background/controllers/operation.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -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(); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
							
								
								
									
										18
									
								
								src/background/controllers/setting.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/background/controllers/setting.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -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(); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										11
									
								
								src/background/controllers/version.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/background/controllers/version.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,11 @@ | |||
| import VersionInteractor from '../usecases/version'; | ||||
| 
 | ||||
| export default class VersionController { | ||||
|   constructor() { | ||||
|     this.versionInteractor = new VersionInteractor(); | ||||
|   } | ||||
| 
 | ||||
|   notifyIfUpdated() { | ||||
|     this.versionInteractor.notifyIfUpdated(); | ||||
|   } | ||||
| } | ||||
|  | @ -9,3 +9,4 @@ export default { | |||
|   quit: 'Close the current tab', | ||||
|   quitall: 'Close all tabs', | ||||
| }; | ||||
| 
 | ||||
							
								
								
									
										14
									
								
								src/background/domains/completion-group.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/background/domains/completion-group.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -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; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										24
									
								
								src/background/domains/completion-item.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/background/domains/completion-item.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -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; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										27
									
								
								src/background/domains/completions.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/background/domains/completions.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -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; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										51
									
								
								src/background/domains/setting.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/background/domains/setting.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -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(); | ||||
|  |  | |||
							
								
								
									
										25
									
								
								src/background/infrastructures/content-message-client.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/background/infrastructures/content-message-client.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -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, | ||||
|     }); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										101
									
								
								src/background/infrastructures/content-message-listener.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								src/background/infrastructures/content-message-listener.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -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); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										19
									
								
								src/background/infrastructures/memory-storage.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/background/infrastructures/memory-storage.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -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); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										23
									
								
								src/background/infrastructures/notifier.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/background/infrastructures/notifier.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -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, | ||||
|     }); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										36
									
								
								src/background/presenters/console.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								src/background/presenters/console.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -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, | ||||
|     }); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										12
									
								
								src/background/presenters/indicator.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/background/presenters/indicator.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -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); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										101
									
								
								src/background/presenters/tab.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								src/background/presenters/tab.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -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); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										5
									
								
								src/background/presenters/window.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/background/presenters/window.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -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; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
							
								
								
									
										13
									
								
								src/background/repositories/bookmark.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/background/repositories/bookmark.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -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; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										31
									
								
								src/background/repositories/completions.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/background/repositories/completions.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -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); | ||||
|     }); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										18
									
								
								src/background/repositories/find.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/background/repositories/find.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -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); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
							
								
								
									
										16
									
								
								src/background/repositories/persistent-setting.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/background/repositories/persistent-setting.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -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); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
							
								
								
									
										23
									
								
								src/background/repositories/setting.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/background/repositories/setting.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -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); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										10
									
								
								src/background/repositories/version.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/background/repositories/version.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -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 }; | ||||
							
								
								
									
										29
									
								
								src/background/usecases/addon-enabled.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								src/background/usecases/addon-enabled.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -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); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										108
									
								
								src/background/usecases/command.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								src/background/usecases/command.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -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); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										150
									
								
								src/background/usecases/completions.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										150
									
								
								src/background/usecases/completions.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -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 | ||||
|     })); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										72
									
								
								src/background/usecases/filters.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								src/background/usecases/filters.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -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 | ||||
| }; | ||||
							
								
								
									
										15
									
								
								src/background/usecases/find.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/background/usecases/find.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -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); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										27
									
								
								src/background/usecases/link.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/background/usecases/link.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -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 | ||||
|     }); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										192
									
								
								src/background/usecases/operation.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										192
									
								
								src/background/usecases/operation.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -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); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
|  | @ -1,20 +1,22 @@ | |||
| const normalizeUrl = (args, searchConfig) => { | ||||
|   let concat = args.join(' '); | ||||
| const trimStart = (str) => { | ||||
|   // NOTE String.trimStart is available on Firefox 61
 | ||||
|   return str.replace(/^\s+/, ''); | ||||
| }; | ||||
| 
 | ||||
| const normalizeUrl = (keywords, searchSettings) => { | ||||
|   try { | ||||
|     return new URL(concat).href; | ||||
|     return new URL(keywords).href; | ||||
|   } catch (e) { | ||||
|     if (concat.includes('.') && !concat.includes(' ')) { | ||||
|       return 'http://' + concat; | ||||
|     if (keywords.includes('.') && !keywords.includes(' ')) { | ||||
|       return 'http://' + keywords; | ||||
|     } | ||||
|     let query = concat; | ||||
|     let template = searchConfig.engines[ | ||||
|       searchConfig.default | ||||
|     ]; | ||||
|     for (let key in searchConfig.engines) { | ||||
|       if (args[0] === key) { | ||||
|         query = args.slice(1).join(' '); | ||||
|         template = searchConfig.engines[key]; | ||||
|       } | ||||
|     let template = searchSettings.engines[searchSettings.default]; | ||||
|     let query = keywords; | ||||
| 
 | ||||
|     let first = trimStart(keywords).split(' ')[0]; | ||||
|     if (Object.keys(searchSettings.engines).includes(first)) { | ||||
|       template = searchSettings.engines[first]; | ||||
|       query = trimStart(trimStart(keywords).slice(first.length)); | ||||
|     } | ||||
|     return template.replace('{}', encodeURIComponent(query)); | ||||
|   } | ||||
|  | @ -50,10 +52,4 @@ const parseSetOption = (word, types) => { | |||
|   } | ||||
| }; | ||||
| 
 | ||||
| const parseCommandLine = (line) => { | ||||
|   let words = line.trim().split(/ +/); | ||||
|   let name = words.shift(); | ||||
|   return [name, words]; | ||||
| }; | ||||
| 
 | ||||
| export { normalizeUrl, parseCommandLine, parseSetOption }; | ||||
| export { normalizeUrl, parseSetOption }; | ||||
							
								
								
									
										31
									
								
								src/background/usecases/setting.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/background/usecases/setting.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -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; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										41
									
								
								src/background/usecases/version.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/background/usecases/version.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -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); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| 
 | ||||
							
								
								
									
										46
									
								
								test/background/infrastructures/memory-storage.test.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								test/background/infrastructures/memory-storage.test.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -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,14 +1,20 @@ | |||
| import * as storage from 'background/shared/versions/storage'; | ||||
| import VersionRepository from 'background/repositories/version'; | ||||
| 
 | ||||
| describe("shared/versions/storage", () => { | ||||
|   describe('#load', () => { | ||||
| describe("background/repositories/version", () => { | ||||
|   let versionRepository; | ||||
| 
 | ||||
|   beforeEach(() => { | ||||
|     versionRepository = new VersionRepository; | ||||
|   }); | ||||
| 
 | ||||
|   describe('#get', () => { | ||||
|     beforeEach(() => { | ||||
|       return browser.storage.local.remove('version'); | ||||
|     }); | ||||
| 
 | ||||
|     it('loads saved version', async() => { | ||||
|       await browser.storage.local.set({ version: '1.2.3' }); | ||||
|       let version = await storage.load(); | ||||
|       let version = await this.versionRepository.get(); | ||||
|       expect(version).to.equal('1.2.3'); | ||||
|     }); | ||||
| 
 | ||||
|  | @ -18,9 +24,9 @@ describe("shared/versions/storage", () => { | |||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('#save', () => { | ||||
|   describe('#update', () => { | ||||
|     it('saves version string', async() => { | ||||
|       await storage.save('2.3.4'); | ||||
|       await versionRepository.update('2.3.4'); | ||||
|       let { version } = await browser.storage.local.get('version'); | ||||
|       expect(version).to.equal('2.3.4'); | ||||
|     }); | ||||
|  | @ -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); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										113
									
								
								test/background/usecases/filters.test.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								test/background/usecases/filters.test.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -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' }, | ||||
|       ]); | ||||
|     }); | ||||
|   }) | ||||
| }); | ||||
|  | @ -1,4 +1,4 @@ | |||
| import * as parsers from 'shared/commands/parsers'; | ||||
| import * as parsers from 'background/usecases/parsers'; | ||||
| 
 | ||||
| describe("shared/commands/parsers", () => { | ||||
|   describe("#parsers.parseSetOption", () => { | ||||
|  | @ -55,30 +55,19 @@ describe("shared/commands/parsers", () => { | |||
|     }; | ||||
| 
 | ||||
|     it('convertes search url', () => { | ||||
|       expect(parsers.normalizeUrl(['google', 'apple'], config)) | ||||
|       expect(parsers.normalizeUrl('google apple', config)) | ||||
|         .to.equal('https://google.com/search?q=apple'); | ||||
|       expect(parsers.normalizeUrl(['yahoo', 'apple'], config)) | ||||
|       expect(parsers.normalizeUrl('yahoo apple', config)) | ||||
|         .to.equal('https://yahoo.com/search?q=apple'); | ||||
|       expect(parsers.normalizeUrl(['google', 'apple', 'banana'], config)) | ||||
|       expect(parsers.normalizeUrl('google apple banana', config)) | ||||
|         .to.equal('https://google.com/search?q=apple%20banana'); | ||||
|       expect(parsers.normalizeUrl(['yahoo', 'C++CLI'], config)) | ||||
|       expect(parsers.normalizeUrl('yahoo C++CLI', config)) | ||||
|         .to.equal('https://yahoo.com/search?q=C%2B%2BCLI'); | ||||
|     }); | ||||
| 
 | ||||
|     it('user default  search engine', () => { | ||||
|       expect(parsers.normalizeUrl(['apple', 'banana'], config)) | ||||
|       expect(parsers.normalizeUrl('apple banana', config)) | ||||
|         .to.equal('https://google.com/search?q=apple%20banana'); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('#parseCommandLine', () => { | ||||
|     it('parse command line as name and args', () => { | ||||
|       expect(parsers.parseCommandLine('open google apple')).to.deep.equal(['open', ['google', 'apple']]); | ||||
|       expect(parsers.parseCommandLine('  open  google  apple  ')).to.deep.equal(['open', ['google', 'apple']]); | ||||
|       expect(parsers.parseCommandLine('')).to.deep.equal(['', []]); | ||||
|       expect(parsers.parseCommandLine('  ')).to.deep.equal(['', []]); | ||||
|       expect(parsers.parseCommandLine('exit')).to.deep.equal(['exit', []]); | ||||
|       expect(parsers.parseCommandLine('  exit  ')).to.deep.equal(['exit', []]); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
		Reference in a new issue