API.* groups + async'ify
* API.styles.* * API.usercss.* * API.sync.* * API.worker.* * API.updater.* * simplify db: resolve with result * remove API.download * simplify download() * remove noCode param as it wastes more time/memory on copying * styleManager: switch style<->data names to reflect their actual contents * inline method bodies to avoid indirection and enable better autocomplete/hint/jump support in IDE
This commit is contained in:
		
							parent
							
								
									06823bd5b4
								
							
						
					
					
						commit
						86623a9aab
					
				|  | @ -4,6 +4,7 @@ | ||||||
| importScripts('/js/worker-util.js'); | importScripts('/js/worker-util.js'); | ||||||
| const {loadScript} = workerUtil; | const {loadScript} = workerUtil; | ||||||
| 
 | 
 | ||||||
|  | /** @namespace ApiWorker */ | ||||||
| workerUtil.createAPI({ | workerUtil.createAPI({ | ||||||
|   parseMozFormat(arg) { |   parseMozFormat(arg) { | ||||||
|     loadScript('/vendor-overwrites/csslint/parserlib.js', '/js/moz-parser.js'); |     loadScript('/vendor-overwrites/csslint/parserlib.js', '/js/moz-parser.js'); | ||||||
|  |  | ||||||
|  | @ -1,49 +1,30 @@ | ||||||
| /* global download prefs openURL FIREFOX CHROME | /* global | ||||||
|   URLS ignoreChromeError chromeLocal semverCompare |   activateTab | ||||||
|   styleManager msg navigatorUtil workerUtil contentScripts sync |   API | ||||||
|   findExistingTab activateTab isTabReplaceable getActiveTab |   chromeLocal | ||||||
|  |   findExistingTab | ||||||
|  |   FIREFOX | ||||||
|  |   getActiveTab | ||||||
|  |   isTabReplaceable | ||||||
|  |   msg | ||||||
|  |   openURL | ||||||
|  |   prefs | ||||||
|  |   semverCompare | ||||||
|  |   URLS | ||||||
|  |   workerUtil | ||||||
| */ | */ | ||||||
| 
 |  | ||||||
| 'use strict'; | 'use strict'; | ||||||
| 
 | 
 | ||||||
| // eslint-disable-next-line no-var
 | //#region API
 | ||||||
| var backgroundWorker = workerUtil.createWorker({ |  | ||||||
|   url: '/background/background-worker.js', |  | ||||||
| }); |  | ||||||
| 
 | 
 | ||||||
| // eslint-disable-next-line no-var
 | Object.assign(API, { | ||||||
| var browserCommands, contextMenus; |  | ||||||
| 
 | 
 | ||||||
| // *************************************************************************
 |   /** @type {ApiWorker} */ | ||||||
| // browser commands
 |   worker: workerUtil.createWorker({ | ||||||
| browserCommands = { |     url: '/background/background-worker.js', | ||||||
|   openManage, |   }), | ||||||
|   openOptions: () => openManage({options: true}), |  | ||||||
|   styleDisableAll(info) { |  | ||||||
|     prefs.set('disableAll', info ? info.checked : !prefs.get('disableAll')); |  | ||||||
|   }, |  | ||||||
|   reload: () => chrome.runtime.reload(), |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| window.API_METHODS = Object.assign(window.API_METHODS || {}, { |  | ||||||
|   deleteStyle: styleManager.deleteStyle, |  | ||||||
|   editSave: styleManager.editSave, |  | ||||||
|   findStyle: styleManager.findStyle, |  | ||||||
|   getAllStyles: styleManager.getAllStyles, // used by importer
 |  | ||||||
|   getSectionsByUrl: styleManager.getSectionsByUrl, |  | ||||||
|   getStyle: styleManager.get, |  | ||||||
|   getStylesByUrl: styleManager.getStylesByUrl, |  | ||||||
|   importStyle: styleManager.importStyle, |  | ||||||
|   importManyStyles: styleManager.importMany, |  | ||||||
|   installStyle: styleManager.installStyle, |  | ||||||
|   styleExists: styleManager.styleExists, |  | ||||||
|   toggleStyle: styleManager.toggleStyle, |  | ||||||
| 
 |  | ||||||
|   addInclusion: styleManager.addInclusion, |  | ||||||
|   removeInclusion: styleManager.removeInclusion, |  | ||||||
|   addExclusion: styleManager.addExclusion, |  | ||||||
|   removeExclusion: styleManager.removeExclusion, |  | ||||||
| 
 | 
 | ||||||
|  |   /** @returns {string} */ | ||||||
|   getTabUrlPrefix() { |   getTabUrlPrefix() { | ||||||
|     const {url} = this.sender.tab; |     const {url} = this.sender.tab; | ||||||
|     if (url.startsWith(URLS.ownOrigin)) { |     if (url.startsWith(URLS.ownOrigin)) { | ||||||
|  | @ -52,21 +33,68 @@ window.API_METHODS = Object.assign(window.API_METHODS || {}, { | ||||||
|     return url.match(/^([\w-]+:\/+[^/#]+)/)[1]; |     return url.match(/^([\w-]+:\/+[^/#]+)/)[1]; | ||||||
|   }, |   }, | ||||||
| 
 | 
 | ||||||
|   download(msg) { |   /** @returns {Prefs} */ | ||||||
|     delete msg.method; |  | ||||||
|     return download(msg.url, msg); |  | ||||||
|   }, |  | ||||||
|   parseCss({code}) { |  | ||||||
|     return backgroundWorker.parseMozFormat({code}); |  | ||||||
|   }, |  | ||||||
|   getPrefs: () => prefs.values, |   getPrefs: () => prefs.values, | ||||||
|   setPref: (key, value) => prefs.set(key, value), |   setPref(key, value) { | ||||||
|  |     prefs.set(key, value); | ||||||
|  |   }, | ||||||
| 
 | 
 | ||||||
|   openEditor, |   /** | ||||||
|  |    * Opens the editor or activates an existing tab | ||||||
|  |    * @param {{ | ||||||
|  |        id?: number | ||||||
|  |        domain?: string | ||||||
|  |        'url-prefix'?: string | ||||||
|  |      }} params | ||||||
|  |    * @returns {Promise<chrome.tabs.Tab>} | ||||||
|  |    */ | ||||||
|  |   openEditor(params) { | ||||||
|  |     const u = new URL(chrome.runtime.getURL('edit.html')); | ||||||
|  |     u.search = new URLSearchParams(params); | ||||||
|  |     return openURL({ | ||||||
|  |       url: `${u}`, | ||||||
|  |       currentWindow: null, | ||||||
|  |       newWindow: prefs.get('openEditInWindow') && Object.assign({}, | ||||||
|  |         prefs.get('openEditInWindow.popup') && {type: 'popup'}, | ||||||
|  |         prefs.get('windowPosition')), | ||||||
|  |     }); | ||||||
|  |   }, | ||||||
| 
 | 
 | ||||||
|   /* Same as openURL, the only extra prop in `opts` is `message` - it'll be sent when the tab is ready, |   /** @returns {Promise<chrome.tabs.Tab>} */ | ||||||
|   which is needed in the popup, otherwise another extension could force the tab to open in foreground |   async openManage({options = false, search, searchMode} = {}) { | ||||||
|   thus auto-closing the popup (in Chrome at least) and preventing the sendMessage code from running */ |     let url = chrome.runtime.getURL('manage.html'); | ||||||
|  |     if (search) { | ||||||
|  |       url += `?search=${encodeURIComponent(search)}&searchMode=${searchMode}`; | ||||||
|  |     } | ||||||
|  |     if (options) { | ||||||
|  |       url += '#stylus-options'; | ||||||
|  |     } | ||||||
|  |     let tab = await findExistingTab({ | ||||||
|  |       url, | ||||||
|  |       currentWindow: null, | ||||||
|  |       ignoreHash: true, | ||||||
|  |       ignoreSearch: true, | ||||||
|  |     }); | ||||||
|  |     if (tab) { | ||||||
|  |       await activateTab(tab); | ||||||
|  |       if (url !== (tab.pendingUrl || tab.url)) { | ||||||
|  |         await msg.sendTab(tab.id, {method: 'pushState', url}).catch(console.error); | ||||||
|  |       } | ||||||
|  |       return tab; | ||||||
|  |     } | ||||||
|  |     tab = await getActiveTab(); | ||||||
|  |     return isTabReplaceable(tab, url) | ||||||
|  |       ? activateTab(tab, {url}) | ||||||
|  |       : browser.tabs.create({url}); | ||||||
|  |   }, | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Same as openURL, the only extra prop in `opts` is `message` - it'll be sent | ||||||
|  |    * when the tab is ready, which is needed in the popup, otherwise another | ||||||
|  |    * extension could force the tab to open in foreground thus auto-closing the | ||||||
|  |    * popup (in Chrome at least) and preventing the sendMessage code from running | ||||||
|  |    * @returns {Promise<chrome.tabs.Tab>} | ||||||
|  |    */ | ||||||
|   async openURL(opts) { |   async openURL(opts) { | ||||||
|     const tab = await openURL(opts); |     const tab = await openURL(opts); | ||||||
|     if (opts.message) { |     if (opts.message) { | ||||||
|  | @ -86,54 +114,49 @@ window.API_METHODS = Object.assign(window.API_METHODS || {}, { | ||||||
|         })); |         })); | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|  | }); | ||||||
| 
 | 
 | ||||||
|   optionsCustomizeHotkeys() { | //#endregion
 | ||||||
|     return browserCommands.openOptions() | //#region browserCommands
 | ||||||
|       .then(() => new Promise(resolve => setTimeout(resolve, 500))) | 
 | ||||||
|       .then(() => msg.broadcastExtension({method: 'optionsCustomizeHotkeys'})); | const browserCommands = { | ||||||
|  |   openManage: () => API.openManage(), | ||||||
|  |   openOptions: () => API.openManage({options: true}), | ||||||
|  |   styleDisableAll(info) { | ||||||
|  |     prefs.set('disableAll', info ? info.checked : !prefs.get('disableAll')); | ||||||
|   }, |   }, | ||||||
| 
 |   reload: () => chrome.runtime.reload(), | ||||||
|   syncStart: sync.start, | }; | ||||||
|   syncStop: sync.stop, | if (chrome.commands) { | ||||||
|   syncNow: sync.syncNow, |   chrome.commands.onCommand.addListener(command => browserCommands[command]()); | ||||||
|   getSyncStatus: sync.getStatus, | } | ||||||
|   syncLogin: sync.login, | if (FIREFOX && browser.commands && browser.commands.update) { | ||||||
| 
 |   // register hotkeys in FF
 | ||||||
|   openManage, |   const hotkeyPrefs = Object.keys(prefs.defaults).filter(k => k.startsWith('hotkey.')); | ||||||
| }); |   prefs.subscribe(hotkeyPrefs, (name, value) => { | ||||||
| 
 |     try { | ||||||
| // *************************************************************************
 |       name = name.split('.')[1]; | ||||||
| // register all listeners
 |       if (value.trim()) { | ||||||
| msg.on(onRuntimeMessage); |         browser.commands.update({name, shortcut: value}); | ||||||
| 
 |       } else { | ||||||
| // tell apply.js to refresh styles for non-committed navigation
 |         browser.commands.reset(name); | ||||||
| navigatorUtil.onUrlChange(({tabId, frameId}, type) => { |       } | ||||||
|   if (type !== 'committed') { |     } catch (e) {} | ||||||
|     msg.sendTab(tabId, {method: 'urlChanged'}, {frameId}) |  | ||||||
|       .catch(msg.ignoreError); |  | ||||||
|   } |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| if (FIREFOX) { |  | ||||||
|   // FF misses some about:blank iframes so we inject our content script explicitly
 |  | ||||||
|   navigatorUtil.onDOMContentLoaded(webNavIframeHelperFF, { |  | ||||||
|     url: [ |  | ||||||
|       {urlEquals: 'about:blank'}, |  | ||||||
|     ], |  | ||||||
|   }); |   }); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| if (chrome.contextMenus) { | //#endregion
 | ||||||
|   chrome.contextMenus.onClicked.addListener((info, tab) => | //#region Init
 | ||||||
|     contextMenus[info.menuItemId].click(info, tab)); |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| if (chrome.commands) { | msg.on((msg, sender) => { | ||||||
|   // Not available in Firefox - https://bugzilla.mozilla.org/show_bug.cgi?id=1240350
 |   if (msg.method === 'invokeAPI') { | ||||||
|   chrome.commands.onCommand.addListener(command => browserCommands[command]()); |     const fn = msg.path.reduce((res, name) => res && res[name], API); | ||||||
| } |     if (!fn) throw new Error(`Unknown API.${msg.path.join('.')}`); | ||||||
|  |     const res = fn.apply({msg, sender}, msg.args); | ||||||
|  |     return res === undefined ? null : res; | ||||||
|  |   } | ||||||
|  | }); | ||||||
| 
 | 
 | ||||||
| // *************************************************************************
 |  | ||||||
| chrome.runtime.onInstalled.addListener(({reason, previousVersion}) => { | chrome.runtime.onInstalled.addListener(({reason, previousVersion}) => { | ||||||
|   if (reason !== 'update') return; |   if (reason !== 'update') return; | ||||||
|   if (semverCompare(previousVersion, '1.5.13') <= 0) { |   if (semverCompare(previousVersion, '1.5.13') <= 0) { | ||||||
|  | @ -150,188 +173,6 @@ chrome.runtime.onInstalled.addListener(({reason, previousVersion}) => { | ||||||
|   } |   } | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| // *************************************************************************
 |  | ||||||
| // context menus
 |  | ||||||
| contextMenus = { |  | ||||||
|   'show-badge': { |  | ||||||
|     title: 'menuShowBadge', |  | ||||||
|     click: info => prefs.set(info.menuItemId, info.checked), |  | ||||||
|   }, |  | ||||||
|   'disableAll': { |  | ||||||
|     title: 'disableAllStyles', |  | ||||||
|     click: browserCommands.styleDisableAll, |  | ||||||
|   }, |  | ||||||
|   'open-manager': { |  | ||||||
|     title: 'openStylesManager', |  | ||||||
|     click: browserCommands.openManage, |  | ||||||
|   }, |  | ||||||
|   'open-options': { |  | ||||||
|     title: 'openOptions', |  | ||||||
|     click: browserCommands.openOptions, |  | ||||||
|   }, |  | ||||||
|   'reload': { |  | ||||||
|     presentIf: async () => (await browser.management.getSelf()).installType === 'development', |  | ||||||
|     title: 'reload', |  | ||||||
|     click: browserCommands.reload, |  | ||||||
|   }, |  | ||||||
|   'editor.contextDelete': { |  | ||||||
|     presentIf: () => !FIREFOX && prefs.get('editor.contextDelete'), |  | ||||||
|     title: 'editDeleteText', |  | ||||||
|     type: 'normal', |  | ||||||
|     contexts: ['editable'], |  | ||||||
|     documentUrlPatterns: [URLS.ownOrigin + 'edit*'], |  | ||||||
|     click: (info, tab) => { |  | ||||||
|       msg.sendTab(tab.id, {method: 'editDeleteText'}, undefined, 'extension') |  | ||||||
|         .catch(msg.ignoreError); |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| async function createContextMenus(ids) { |  | ||||||
|   for (const id of ids) { |  | ||||||
|     let item = contextMenus[id]; |  | ||||||
|     if (item.presentIf && !await item.presentIf()) { |  | ||||||
|       continue; |  | ||||||
|     } |  | ||||||
|     item = Object.assign({id}, item); |  | ||||||
|     delete item.presentIf; |  | ||||||
|     item.title = chrome.i18n.getMessage(item.title); |  | ||||||
|     if (!item.type && typeof prefs.defaults[id] === 'boolean') { |  | ||||||
|       item.type = 'checkbox'; |  | ||||||
|       item.checked = prefs.get(id); |  | ||||||
|     } |  | ||||||
|     if (!item.contexts) { |  | ||||||
|       item.contexts = ['browser_action']; |  | ||||||
|     } |  | ||||||
|     delete item.click; |  | ||||||
|     chrome.contextMenus.create(item, ignoreChromeError); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| if (chrome.contextMenus) { |  | ||||||
|   // "Delete" item in context menu for browsers that don't have it
 |  | ||||||
|   if (CHROME && |  | ||||||
|       // looking at the end of UA string
 |  | ||||||
|       /(Vivaldi|Safari)\/[\d.]+$/.test(navigator.userAgent) && |  | ||||||
|       // skip forks with Flash as those are likely to have the menu e.g. CentBrowser
 |  | ||||||
|       !Array.from(navigator.plugins).some(p => p.name === 'Shockwave Flash')) { |  | ||||||
|     prefs.defaults['editor.contextDelete'] = true; |  | ||||||
|   } |  | ||||||
|   // circumvent the bug with disabling check marks in Chrome 62-64
 |  | ||||||
|   const toggleCheckmark = CHROME >= 62 && CHROME <= 64 ? |  | ||||||
|     (id => chrome.contextMenus.remove(id, () => createContextMenus([id]) + ignoreChromeError())) : |  | ||||||
|     ((id, checked) => chrome.contextMenus.update(id, {checked}, ignoreChromeError)); |  | ||||||
| 
 |  | ||||||
|   const togglePresence = (id, checked) => { |  | ||||||
|     if (checked) { |  | ||||||
|       createContextMenus([id]); |  | ||||||
|     } else { |  | ||||||
|       chrome.contextMenus.remove(id, ignoreChromeError); |  | ||||||
|     } |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   const keys = Object.keys(contextMenus); |  | ||||||
|   prefs.subscribe(keys.filter(id => typeof prefs.defaults[id] === 'boolean'), toggleCheckmark); |  | ||||||
|   prefs.subscribe(keys.filter(id => contextMenus[id].presentIf && id in prefs.defaults), togglePresence); |  | ||||||
|   createContextMenus(keys); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // reinject content scripts when the extension is reloaded/updated. Firefox
 |  | ||||||
| // would handle this automatically.
 |  | ||||||
| if (!FIREFOX) { |  | ||||||
|   setTimeout(contentScripts.injectToAllTabs, 0); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // register hotkeys
 |  | ||||||
| if (FIREFOX && browser.commands && browser.commands.update) { |  | ||||||
|   const hotkeyPrefs = Object.keys(prefs.defaults).filter(k => k.startsWith('hotkey.')); |  | ||||||
|   prefs.subscribe(hotkeyPrefs, (name, value) => { |  | ||||||
|     try { |  | ||||||
|       name = name.split('.')[1]; |  | ||||||
|       if (value.trim()) { |  | ||||||
|         browser.commands.update({name, shortcut: value}); |  | ||||||
|       } else { |  | ||||||
|         browser.commands.reset(name); |  | ||||||
|       } |  | ||||||
|     } catch (e) {} |  | ||||||
|   }); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| msg.broadcast({method: 'backgroundReady'}); | msg.broadcast({method: 'backgroundReady'}); | ||||||
| 
 | 
 | ||||||
| function webNavIframeHelperFF({tabId, frameId}) { | //#endregion
 | ||||||
|   if (!frameId) return; |  | ||||||
|   msg.sendTab(tabId, {method: 'ping'}, {frameId}) |  | ||||||
|     .catch(() => false) |  | ||||||
|     .then(pong => { |  | ||||||
|       if (pong) return; |  | ||||||
|       // insert apply.js to iframe
 |  | ||||||
|       const files = chrome.runtime.getManifest().content_scripts[0].js; |  | ||||||
|       for (const file of files) { |  | ||||||
|         chrome.tabs.executeScript(tabId, { |  | ||||||
|           frameId, |  | ||||||
|           file, |  | ||||||
|           matchAboutBlank: true, |  | ||||||
|         }, ignoreChromeError); |  | ||||||
|       } |  | ||||||
|     }); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| function onRuntimeMessage(msg, sender) { |  | ||||||
|   if (msg.method !== 'invokeAPI') { |  | ||||||
|     return; |  | ||||||
|   } |  | ||||||
|   const fn = window.API_METHODS[msg.name]; |  | ||||||
|   if (!fn) { |  | ||||||
|     throw new Error(`unknown API: ${msg.name}`); |  | ||||||
|   } |  | ||||||
|   const res = fn.apply({msg, sender}, msg.args); |  | ||||||
|   return res === undefined ? null : res; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| function openEditor(params) { |  | ||||||
|   /* Open the editor. Activate if it is already opened |  | ||||||
| 
 |  | ||||||
|   params: { |  | ||||||
|     id?: Number, |  | ||||||
|     domain?: String, |  | ||||||
|     'url-prefix'?: String |  | ||||||
|   } |  | ||||||
|   */ |  | ||||||
|   const u = new URL(chrome.runtime.getURL('edit.html')); |  | ||||||
|   u.search = new URLSearchParams(params); |  | ||||||
|   return openURL({ |  | ||||||
|     url: `${u}`, |  | ||||||
|     currentWindow: null, |  | ||||||
|     newWindow: prefs.get('openEditInWindow') && Object.assign({}, |  | ||||||
|       prefs.get('openEditInWindow.popup') && {type: 'popup'}, |  | ||||||
|       prefs.get('windowPosition')), |  | ||||||
|   }); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| async function openManage({options = false, search, searchMode} = {}) { |  | ||||||
|   let url = chrome.runtime.getURL('manage.html'); |  | ||||||
|   if (search) { |  | ||||||
|     url += `?search=${encodeURIComponent(search)}&searchMode=${searchMode}`; |  | ||||||
|   } |  | ||||||
|   if (options) { |  | ||||||
|     url += '#stylus-options'; |  | ||||||
|   } |  | ||||||
|   let tab = await findExistingTab({ |  | ||||||
|     url, |  | ||||||
|     currentWindow: null, |  | ||||||
|     ignoreHash: true, |  | ||||||
|     ignoreSearch: true, |  | ||||||
|   }); |  | ||||||
|   if (tab) { |  | ||||||
|     await activateTab(tab); |  | ||||||
|     if (url !== (tab.pendingUrl || tab.url)) { |  | ||||||
|       await msg.sendTab(tab.id, {method: 'pushState', url}).catch(console.error); |  | ||||||
|     } |  | ||||||
|     return tab; |  | ||||||
|   } |  | ||||||
|   tab = await getActiveTab(); |  | ||||||
|   return isTabReplaceable(tab, url) |  | ||||||
|     ? activateTab(tab, {url}) |  | ||||||
|     : browser.tabs.create({url}); |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -1,8 +1,18 @@ | ||||||
| /* global msg ignoreChromeError URLS */ | /* global | ||||||
| /* exported contentScripts */ |   FIREFOX | ||||||
|  |   ignoreChromeError | ||||||
|  |   msg | ||||||
|  |   URLS | ||||||
|  | */ | ||||||
| 'use strict'; | 'use strict'; | ||||||
| 
 | 
 | ||||||
| const contentScripts = (() => { | /* | ||||||
|  |  Reinject content scripts when the extension is reloaded/updated. | ||||||
|  |  Firefox handles this automatically. | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | // eslint-disable-next-line no-unused-expressions
 | ||||||
|  | !FIREFOX && (() => { | ||||||
|   const NTP = 'chrome://newtab/'; |   const NTP = 'chrome://newtab/'; | ||||||
|   const ALL_URLS = '<all_urls>'; |   const ALL_URLS = '<all_urls>'; | ||||||
|   const SCRIPTS = chrome.runtime.getManifest().content_scripts; |   const SCRIPTS = chrome.runtime.getManifest().content_scripts; | ||||||
|  | @ -18,21 +28,7 @@ const contentScripts = (() => { | ||||||
|   const busyTabs = new Set(); |   const busyTabs = new Set(); | ||||||
|   let busyTabsTimer; |   let busyTabsTimer; | ||||||
| 
 | 
 | ||||||
|   // expose version on greasyfork/sleazyfork 1) info page and 2) code page
 |   setTimeout(injectToAllTabs); | ||||||
|   const urlMatches = '/scripts/\\d+[^/]*(/code)?([?#].*)?$'; |  | ||||||
|   chrome.webNavigation.onCommitted.addListener(({tabId}) => { |  | ||||||
|     chrome.tabs.executeScript(tabId, { |  | ||||||
|       file: '/content/install-hook-greasyfork.js', |  | ||||||
|       runAt: 'document_start', |  | ||||||
|     }); |  | ||||||
|   }, { |  | ||||||
|     url: [ |  | ||||||
|       {hostEquals: 'greasyfork.org', urlMatches}, |  | ||||||
|       {hostEquals: 'sleazyfork.org', urlMatches}, |  | ||||||
|     ], |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   return {injectToTab, injectToAllTabs}; |  | ||||||
| 
 | 
 | ||||||
|   function injectToTab({url, tabId, frameId = null}) { |   function injectToTab({url, tabId, frameId = null}) { | ||||||
|     for (const script of SCRIPTS) { |     for (const script of SCRIPTS) { | ||||||
|  |  | ||||||
							
								
								
									
										107
									
								
								background/context-menus.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								background/context-menus.js
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,107 @@ | ||||||
|  | /* global | ||||||
|  |   browserCommands | ||||||
|  |   CHROME | ||||||
|  |   FIREFOX | ||||||
|  |   ignoreChromeError | ||||||
|  |   msg | ||||||
|  |   prefs | ||||||
|  |   URLS | ||||||
|  | */ | ||||||
|  | 'use strict'; | ||||||
|  | 
 | ||||||
|  | // eslint-disable-next-line no-unused-expressions
 | ||||||
|  | chrome.contextMenus && (() => { | ||||||
|  |   const contextMenus = { | ||||||
|  |     'show-badge': { | ||||||
|  |       title: 'menuShowBadge', | ||||||
|  |       click: info => prefs.set(info.menuItemId, info.checked), | ||||||
|  |     }, | ||||||
|  |     'disableAll': { | ||||||
|  |       title: 'disableAllStyles', | ||||||
|  |       click: browserCommands.styleDisableAll, | ||||||
|  |     }, | ||||||
|  |     'open-manager': { | ||||||
|  |       title: 'openStylesManager', | ||||||
|  |       click: browserCommands.openManage, | ||||||
|  |     }, | ||||||
|  |     'open-options': { | ||||||
|  |       title: 'openOptions', | ||||||
|  |       click: browserCommands.openOptions, | ||||||
|  |     }, | ||||||
|  |     'reload': { | ||||||
|  |       presentIf: async () => (await browser.management.getSelf()).installType === 'development', | ||||||
|  |       title: 'reload', | ||||||
|  |       click: browserCommands.reload, | ||||||
|  |     }, | ||||||
|  |     'editor.contextDelete': { | ||||||
|  |       presentIf: () => !FIREFOX && prefs.get('editor.contextDelete'), | ||||||
|  |       title: 'editDeleteText', | ||||||
|  |       type: 'normal', | ||||||
|  |       contexts: ['editable'], | ||||||
|  |       documentUrlPatterns: [URLS.ownOrigin + 'edit*'], | ||||||
|  |       click: (info, tab) => { | ||||||
|  |         msg.sendTab(tab.id, {method: 'editDeleteText'}, undefined, 'extension') | ||||||
|  |           .catch(msg.ignoreError); | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   // "Delete" item in context menu for browsers that don't have it
 | ||||||
|  |   if (CHROME && | ||||||
|  |       // looking at the end of UA string
 | ||||||
|  |       /(Vivaldi|Safari)\/[\d.]+$/.test(navigator.userAgent) && | ||||||
|  |       // skip forks with Flash as those are likely to have the menu e.g. CentBrowser
 | ||||||
|  |       !Array.from(navigator.plugins).some(p => p.name === 'Shockwave Flash')) { | ||||||
|  |     prefs.defaults['editor.contextDelete'] = true; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   const keys = Object.keys(contextMenus); | ||||||
|  |   prefs.subscribe(keys.filter(id => typeof prefs.defaults[id] === 'boolean'), | ||||||
|  |     CHROME >= 62 && CHROME <= 64 ? toggleCheckmarkBugged : toggleCheckmark); | ||||||
|  |   prefs.subscribe(keys.filter(id => contextMenus[id].presentIf && id in prefs.defaults), | ||||||
|  |     togglePresence); | ||||||
|  | 
 | ||||||
|  |   createContextMenus(keys); | ||||||
|  | 
 | ||||||
|  |   chrome.contextMenus.onClicked.addListener((info, tab) => | ||||||
|  |     contextMenus[info.menuItemId].click(info, tab)); | ||||||
|  | 
 | ||||||
|  |   async function createContextMenus(ids) { | ||||||
|  |     for (const id of ids) { | ||||||
|  |       let item = contextMenus[id]; | ||||||
|  |       if (item.presentIf && !await item.presentIf()) { | ||||||
|  |         continue; | ||||||
|  |       } | ||||||
|  |       item = Object.assign({id}, item); | ||||||
|  |       delete item.presentIf; | ||||||
|  |       item.title = chrome.i18n.getMessage(item.title); | ||||||
|  |       if (!item.type && typeof prefs.defaults[id] === 'boolean') { | ||||||
|  |         item.type = 'checkbox'; | ||||||
|  |         item.checked = prefs.get(id); | ||||||
|  |       } | ||||||
|  |       if (!item.contexts) { | ||||||
|  |         item.contexts = ['browser_action']; | ||||||
|  |       } | ||||||
|  |       delete item.click; | ||||||
|  |       chrome.contextMenus.create(item, ignoreChromeError); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   function toggleCheckmark(id, checked) { | ||||||
|  |     chrome.contextMenus.update(id, {checked}, ignoreChromeError); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** Circumvents the bug with disabling check marks in Chrome 62-64 */ | ||||||
|  |   async function toggleCheckmarkBugged(id) { | ||||||
|  |     await browser.contextMenus.remove(id).catch(ignoreChromeError); | ||||||
|  |     createContextMenus([id]); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   function togglePresence(id, checked) { | ||||||
|  |     if (checked) { | ||||||
|  |       createContextMenus([id]); | ||||||
|  |     } else { | ||||||
|  |       chrome.contextMenus.remove(id, ignoreChromeError); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | })(); | ||||||
|  | @ -35,20 +35,9 @@ function createChromeStorageDB() { | ||||||
|       }), |       }), | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   return {exec}; |   return { | ||||||
| 
 |     exec: (method, ...args) => METHODS[method](...args), | ||||||
|   function exec(method, ...args) { |   }; | ||||||
|     if (METHODS[method]) { |  | ||||||
|       return METHODS[method](...args) |  | ||||||
|         .then(result => { |  | ||||||
|           if (method === 'putMany' && result.map) { |  | ||||||
|             return result.map(r => ({target: {result: r}})); |  | ||||||
|           } |  | ||||||
|           return {target: {result}}; |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
|     return Promise.reject(new Error(`unknown DB method ${method}`)); |  | ||||||
|   } |  | ||||||
| 
 | 
 | ||||||
|   function prepareInc() { |   function prepareInc() { | ||||||
|     if (INC) return Promise.resolve(); |     if (INC) return Promise.resolve(); | ||||||
|  |  | ||||||
|  | @ -33,18 +33,17 @@ const db = (() => { | ||||||
|       case false: break; |       case false: break; | ||||||
|       default: await testDB(); |       default: await testDB(); | ||||||
|     } |     } | ||||||
|     return useIndexedDB(); |     chromeLocal.setValue(FALLBACK, false); | ||||||
|  |     return dbExecIndexedDB; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async function testDB() { |   async function testDB() { | ||||||
|     let e = await dbExecIndexedDB('getAllKeys', IDBKeyRange.lowerBound(1), 1); |     let e = await dbExecIndexedDB('getAllKeys', IDBKeyRange.lowerBound(1), 1); | ||||||
|     // throws if result is null
 |     e = e[0]; // throws if result is null
 | ||||||
|     e = e.target.result[0]; |  | ||||||
|     const id = `${performance.now()}.${Math.random()}.${Date.now()}`; |     const id = `${performance.now()}.${Math.random()}.${Date.now()}`; | ||||||
|     await dbExecIndexedDB('put', {id}); |     await dbExecIndexedDB('put', {id}); | ||||||
|     e = await dbExecIndexedDB('get', id); |     e = await dbExecIndexedDB('get', id); | ||||||
|     // throws if result or id is null
 |     await dbExecIndexedDB('delete', e.id); // throws if `e` or id is null
 | ||||||
|     await dbExecIndexedDB('delete', e.target.result.id); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   function useChromeStorage(err) { |   function useChromeStorage(err) { | ||||||
|  | @ -56,11 +55,6 @@ const db = (() => { | ||||||
|     return createChromeStorageDB().exec; |     return createChromeStorageDB().exec; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   function useIndexedDB() { |  | ||||||
|     chromeLocal.setValue(FALLBACK, false); |  | ||||||
|     return dbExecIndexedDB; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   async function dbExecIndexedDB(method, ...args) { |   async function dbExecIndexedDB(method, ...args) { | ||||||
|     const mode = method.startsWith('get') ? 'readonly' : 'readwrite'; |     const mode = method.startsWith('get') ? 'readonly' : 'readwrite'; | ||||||
|     const store = (await open()).transaction([STORE], mode).objectStore(STORE); |     const store = (await open()).transaction([STORE], mode).objectStore(STORE); | ||||||
|  | @ -70,8 +64,9 @@ const db = (() => { | ||||||
| 
 | 
 | ||||||
|   function storeRequest(store, method, ...args) { |   function storeRequest(store, method, ...args) { | ||||||
|     return new Promise((resolve, reject) => { |     return new Promise((resolve, reject) => { | ||||||
|  |       /** @type {IDBRequest} */ | ||||||
|       const request = store[method](...args); |       const request = store[method](...args); | ||||||
|       request.onsuccess = resolve; |       request.onsuccess = () => resolve(request.result); | ||||||
|       request.onerror = reject; |       request.onerror = reject; | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| /* global prefs debounce iconUtil FIREFOX CHROME VIVALDI tabManager navigatorUtil API_METHODS */ | /* global prefs debounce iconUtil FIREFOX CHROME VIVALDI tabManager navigatorUtil API */ | ||||||
| /* exported iconManager */ | /* exported iconManager */ | ||||||
| 'use strict'; | 'use strict'; | ||||||
| 
 | 
 | ||||||
|  | @ -27,7 +27,7 @@ const iconManager = (() => { | ||||||
|     refreshAllIcons(); |     refreshAllIcons(); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   Object.assign(API_METHODS, { |   Object.assign(API, { | ||||||
|     /** @param {(number|string)[]} styleIds |     /** @param {(number|string)[]} styleIds | ||||||
|      * @param {boolean} [lazyBadge=false] preventing flicker during page load */ |      * @param {boolean} [lazyBadge=false] preventing flicker during page load */ | ||||||
|     updateIconBadge(styleIds, {lazyBadge} = {}) { |     updateIconBadge(styleIds, {lazyBadge} = {}) { | ||||||
|  | @ -53,7 +53,7 @@ const iconManager = (() => { | ||||||
| 
 | 
 | ||||||
|   function onPortDisconnected({sender}) { |   function onPortDisconnected({sender}) { | ||||||
|     if (tabManager.get(sender.tab.id, 'styleIds')) { |     if (tabManager.get(sender.tab.id, 'styleIds')) { | ||||||
|       API_METHODS.updateIconBadge.call({sender}, [], {lazyBadge: true}); |       API.updateIconBadge.call({sender}, [], {lazyBadge: true}); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,75 +1,103 @@ | ||||||
| /* global CHROME URLS */ | /* global | ||||||
| /* exported navigatorUtil */ |   CHROME | ||||||
|  |   FIREFOX | ||||||
|  |   ignoreChromeError | ||||||
|  |   msg | ||||||
|  |   URLS | ||||||
|  | */ | ||||||
| 'use strict'; | 'use strict'; | ||||||
| 
 | 
 | ||||||
| const navigatorUtil = (() => { | (() => { | ||||||
|   const handler = { |   /** @type {Set<function(data: Object, type: string)>} */ | ||||||
|     urlChange: null, |   const listeners = new Set(); | ||||||
|   }; |   /** @type {NavigatorUtil} */ | ||||||
|   return extendNative({onUrlChange}); |   const navigatorUtil = window.navigatorUtil = new Proxy({ | ||||||
|  |     onUrlChange(fn) { | ||||||
|  |       listeners.add(fn); | ||||||
|  |     }, | ||||||
|  |   }, { | ||||||
|  |     get(target, prop) { | ||||||
|  |       return target[prop] || | ||||||
|  |         (target = chrome.webNavigation[prop]).addListener.bind(target); | ||||||
|  |     }, | ||||||
|  |   }); | ||||||
| 
 | 
 | ||||||
|   function onUrlChange(fn) { |   navigatorUtil.onCommitted(onNavigation.bind('committed')); | ||||||
|     initUrlChange(); |   navigatorUtil.onHistoryStateUpdated(onFakeNavigation.bind('history')); | ||||||
|     handler.urlChange.push(fn); |   navigatorUtil.onReferenceFragmentUpdated(onFakeNavigation.bind('hash')); | ||||||
|  |   navigatorUtil.onCommitted(runGreasyforkContentScript, { | ||||||
|  |     // expose style version on greasyfork/sleazyfork 1) info page and 2) code page
 | ||||||
|  |     url: ['greasyfork', 'sleazyfork'].map(host => ({ | ||||||
|  |       hostEquals: host + '.org', | ||||||
|  |       urlMatches: '/scripts/\\d+[^/]*(/code)?([?#].*)?$', | ||||||
|  |     })), | ||||||
|  |   }); | ||||||
|  |   if (FIREFOX) { | ||||||
|  |     navigatorUtil.onDOMContentLoaded(runMainContentScripts, { | ||||||
|  |       url: [{ | ||||||
|  |         urlEquals: 'about:blank', | ||||||
|  |       }], | ||||||
|  |     }); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   function initUrlChange() { |   /** @this {string} type */ | ||||||
|     if (handler.urlChange) { |   async function onNavigation(data) { | ||||||
|       return; |     if (CHROME && | ||||||
|  |         URLS.chromeProtectsNTP && | ||||||
|  |         data.url.startsWith('https://www.google.') && | ||||||
|  |         data.url.includes('/_/chrome/newtab?')) { | ||||||
|  |       // Modern Chrome switched to WebUI NTP so this is obsolete, but there may be exceptions
 | ||||||
|  |       // TODO: investigate, and maybe use a separate listener for CHROME <= ver
 | ||||||
|  |       const tab = await browser.tabs.get(data.tabId); | ||||||
|  |       const url = tab.pendingUrl || tab.url; | ||||||
|  |       if (url === 'chrome://newtab/') { | ||||||
|  |         data.url = url; | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|     handler.urlChange = []; |     listeners.forEach(fn => fn(data, this)); | ||||||
| 
 |  | ||||||
|     chrome.webNavigation.onCommitted.addListener(data => |  | ||||||
|       fixNTPUrl(data) |  | ||||||
|         .then(() => executeCallbacks(handler.urlChange, data, 'committed')) |  | ||||||
|         .catch(console.error) |  | ||||||
|     ); |  | ||||||
| 
 |  | ||||||
|     chrome.webNavigation.onHistoryStateUpdated.addListener(data => |  | ||||||
|       fixNTPUrl(data) |  | ||||||
|         .then(() => executeCallbacks(handler.urlChange, data, 'historyStateUpdated')) |  | ||||||
|         .catch(console.error) |  | ||||||
|     ); |  | ||||||
| 
 |  | ||||||
|     chrome.webNavigation.onReferenceFragmentUpdated.addListener(data => |  | ||||||
|       fixNTPUrl(data) |  | ||||||
|         .then(() => executeCallbacks(handler.urlChange, data, 'referenceFragmentUpdated')) |  | ||||||
|         .catch(console.error) |  | ||||||
|     ); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   function fixNTPUrl(data) { |   /** @this {string} type */ | ||||||
|     if ( |   function onFakeNavigation(data) { | ||||||
|       !CHROME || |     onNavigation.call(this, data); | ||||||
|       !URLS.chromeProtectsNTP || |     msg.sendTab(data.tabId, {method: 'urlChanged'}, {frameId: data.frameId}) | ||||||
|       !data.url.startsWith('https://www.google.') || |       .catch(msg.ignoreError); | ||||||
|       !data.url.includes('/_/chrome/newtab?') |  | ||||||
|     ) { |  | ||||||
|       return Promise.resolve(); |  | ||||||
|     } |  | ||||||
|     return browser.tabs.get(data.tabId) |  | ||||||
|       .then(tab => { |  | ||||||
|         const url = tab.pendingUrl || tab.url; |  | ||||||
|         if (url === 'chrome://newtab/') { |  | ||||||
|           data.url = url; |  | ||||||
|         } |  | ||||||
|       }); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   function executeCallbacks(callbacks, data, type) { |   /** FF misses some about:blank iframes so we inject our content script explicitly */ | ||||||
|     for (const cb of callbacks) { |   async function runMainContentScripts({tabId, frameId}) { | ||||||
|       cb(data, type); |     if (frameId && | ||||||
|  |         !await msg.sendTab(tabId, {method: 'ping'}, {frameId}).catch(ignoreChromeError)) { | ||||||
|  |       for (const file of chrome.runtime.getManifest().content_scripts[0].js) { | ||||||
|  |         chrome.tabs.executeScript(tabId, { | ||||||
|  |           frameId, | ||||||
|  |           file, | ||||||
|  |           matchAboutBlank: true, | ||||||
|  |         }, ignoreChromeError); | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   function extendNative(target) { |   function runGreasyforkContentScript({tabId}) { | ||||||
|     return new Proxy(target, { |     chrome.tabs.executeScript(tabId, { | ||||||
|       get: (target, prop) => { |       file: '/content/install-hook-greasyfork.js', | ||||||
|         if (target[prop]) { |       runAt: 'document_start', | ||||||
|           return target[prop]; |  | ||||||
|         } |  | ||||||
|         return chrome.webNavigation[prop].addListener.bind(chrome.webNavigation[prop]); |  | ||||||
|       }, |  | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
| })(); | })(); | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * @typedef NavigatorUtil | ||||||
|  |  * @property {NavigatorUtilEvent} onBeforeNavigate | ||||||
|  |  * @property {NavigatorUtilEvent} onCommitted | ||||||
|  |  * @property {NavigatorUtilEvent} onCompleted | ||||||
|  |  * @property {NavigatorUtilEvent} onCreatedNavigationTarget | ||||||
|  |  * @property {NavigatorUtilEvent} onDOMContentLoaded | ||||||
|  |  * @property {NavigatorUtilEvent} onErrorOccurred | ||||||
|  |  * @property {NavigatorUtilEvent} onHistoryStateUpdated | ||||||
|  |  * @property {NavigatorUtilEvent} onReferenceFragmentUpdated | ||||||
|  |  * @property {NavigatorUtilEvent} onTabReplaced | ||||||
|  | */ | ||||||
|  | /** | ||||||
|  |  * @typedef {function(cb: function, filters: WebNavigationEventFilter?)} NavigatorUtilEvent | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | @ -1,3 +1,4 @@ | ||||||
|  | /* global API */ | ||||||
| 'use strict'; | 'use strict'; | ||||||
| 
 | 
 | ||||||
| (() => { | (() => { | ||||||
|  | @ -40,7 +41,7 @@ | ||||||
|     .then(res => res.json()); |     .then(res => res.json()); | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   window.API_METHODS = Object.assign(window.API_METHODS || {}, { |   API.openusercss = { | ||||||
|     /** |     /** | ||||||
|      *   This function can be used to retrieve a theme object from the |      *   This function can be used to retrieve a theme object from the | ||||||
|      *   GraphQL API, set above |      *   GraphQL API, set above | ||||||
|  | @ -98,5 +99,5 @@ | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     `),
 |     `),
 | ||||||
|   }); |   }; | ||||||
| })(); | })(); | ||||||
|  |  | ||||||
|  | @ -1,8 +1,7 @@ | ||||||
| /* global | /* global | ||||||
|   API_METHODS |   API | ||||||
|   debounce |   debounce | ||||||
|   stringAsRegExp |   stringAsRegExp | ||||||
|   styleManager |  | ||||||
|   tryRegExp |   tryRegExp | ||||||
|   usercss |   usercss | ||||||
| */ | */ | ||||||
|  | @ -50,16 +49,16 @@ | ||||||
|    * @param {number[]} [params.ids] - if not specified, all styles are searched |    * @param {number[]} [params.ids] - if not specified, all styles are searched | ||||||
|    * @returns {number[]} - array of matched styles ids |    * @returns {number[]} - array of matched styles ids | ||||||
|    */ |    */ | ||||||
|   API_METHODS.searchDB = async ({query, mode = 'all', ids}) => { |   API.searchDB = async ({query, mode = 'all', ids}) => { | ||||||
|     let res = []; |     let res = []; | ||||||
|     if (mode === 'url' && query) { |     if (mode === 'url' && query) { | ||||||
|       res = (await styleManager.getStylesByUrl(query)).map(r => r.data.id); |       res = (await API.styles.getByUrl(query)).map(r => r.style.id); | ||||||
|     } else if (mode in MODES) { |     } else if (mode in MODES) { | ||||||
|       const modeHandler = MODES[mode]; |       const modeHandler = MODES[mode]; | ||||||
|       const m = /^\/(.+?)\/([gimsuy]*)$/.exec(query); |       const m = /^\/(.+?)\/([gimsuy]*)$/.exec(query); | ||||||
|       const rx = m && tryRegExp(m[1], m[2]); |       const rx = m && tryRegExp(m[1], m[2]); | ||||||
|       const test = rx ? rx.test.bind(rx) : makeTester(query); |       const test = rx ? rx.test.bind(rx) : makeTester(query); | ||||||
|       res = (await styleManager.getAllStyles()) |       res = (await API.styles.getAll()) | ||||||
|         .filter(style => |         .filter(style => | ||||||
|           (!ids || ids.includes(style.id)) && |           (!ids || ids.includes(style.id)) && | ||||||
|           (!query || modeHandler(style, test))) |           (!query || modeHandler(style, test))) | ||||||
|  |  | ||||||
|  | @ -1,7 +1,16 @@ | ||||||
| /* eslint no-eq-null: 0, eqeqeq: [2, "smart"] */ | /* global | ||||||
| /* global createCache db calcStyleDigest db tryRegExp styleCodeEmpty styleSectionGlobal |   API | ||||||
|   getStyleWithNoCode msg prefs sync URLS */ |   calcStyleDigest | ||||||
| /* exported styleManager */ |   createCache | ||||||
|  |   db | ||||||
|  |   msg | ||||||
|  |   prefs | ||||||
|  |   stringAsRegExp | ||||||
|  |   styleCodeEmpty | ||||||
|  |   styleSectionGlobal | ||||||
|  |   tryRegExp | ||||||
|  |   URLS | ||||||
|  | */ | ||||||
| 'use strict'; | 'use strict'; | ||||||
| 
 | 
 | ||||||
| /* | /* | ||||||
|  | @ -13,41 +22,34 @@ The live preview feature relies on `runtime.connect` and `port.onDisconnect` | ||||||
| to cleanup the temporary code. See /edit/live-preview.js. | to cleanup the temporary code. See /edit/live-preview.js. | ||||||
| */ | */ | ||||||
| 
 | 
 | ||||||
| /** @type {styleManager} */ | /* exported styleManager */ | ||||||
| const styleManager = (() => { | const styleManager = API.styles = (() => { | ||||||
|   const preparing = prepare(); |  | ||||||
| 
 | 
 | ||||||
|   /* styleId => { |   //#region Declarations
 | ||||||
|     data: styleData, |   const ready = init(); | ||||||
|     preview: styleData, |   /** | ||||||
|     appliesTo: Set<url> |    * @typedef StyleMapData | ||||||
|   } */ |    * @property {StyleObj} style | ||||||
|   const styles = new Map(); |    * @property {?StyleObj} [preview] | ||||||
|  |    * @property {Set<string>} appliesTo - urls | ||||||
|  |    */ | ||||||
|  |   /** @type {Map<number,StyleMapData>} */ | ||||||
|  |   const dataMap = new Map(); | ||||||
|   const uuidIndex = new Map(); |   const uuidIndex = new Map(); | ||||||
| 
 |   /** @typedef {Object<styleId,{id: number, code: string[]}>} StyleSectionsToApply */ | ||||||
|   /* url => { |   /** @type {Map<string,{maybeMatch: Set<styleId>, sections: StyleSectionsToApply}>} */ | ||||||
|     maybeMatch: Set<styleId>, |  | ||||||
|     sections: Object<styleId => { |  | ||||||
|       id: styleId, |  | ||||||
|       code: Array<String> |  | ||||||
|     }> |  | ||||||
|   } */ |  | ||||||
|   const cachedStyleForUrl = createCache({ |   const cachedStyleForUrl = createCache({ | ||||||
|     onDeleted: (url, cache) => { |     onDeleted: (url, cache) => { | ||||||
|       for (const section of Object.values(cache.sections)) { |       for (const section of Object.values(cache.sections)) { | ||||||
|         const style = styles.get(section.id); |         const data = id2data(section.id); | ||||||
|         if (style) { |         if (data) data.appliesTo.delete(url); | ||||||
|           style.appliesTo.delete(url); |  | ||||||
|         } |  | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|   }); |   }); | ||||||
| 
 |  | ||||||
|   const BAD_MATCHER = {test: () => false}; |   const BAD_MATCHER = {test: () => false}; | ||||||
|   const compileRe = createCompiler(text => `^(${text})$`); |   const compileRe = createCompiler(text => `^(${text})$`); | ||||||
|   const compileSloppyRe = createCompiler(text => `^${text}$`); |   const compileSloppyRe = createCompiler(text => `^${text}$`); | ||||||
|   const compileExclusion = createCompiler(buildExclusion); |   const compileExclusion = createCompiler(buildExclusion); | ||||||
| 
 |  | ||||||
|   const DUMMY_URL = { |   const DUMMY_URL = { | ||||||
|     hash: '', |     hash: '', | ||||||
|     host: '', |     host: '', | ||||||
|  | @ -62,287 +64,256 @@ const styleManager = (() => { | ||||||
|     searchParams: new URLSearchParams(), |     searchParams: new URLSearchParams(), | ||||||
|     username: '', |     username: '', | ||||||
|   }; |   }; | ||||||
| 
 |   const MISSING_PROPS = { | ||||||
|  |     name: style => `ID: ${style.id}`, | ||||||
|  |     _id: () => uuidv4(), | ||||||
|  |     _rev: () => Date.now(), | ||||||
|  |   }; | ||||||
|   const DELETE_IF_NULL = ['id', 'customName']; |   const DELETE_IF_NULL = ['id', 'customName']; | ||||||
|  |   //#endregion
 | ||||||
| 
 | 
 | ||||||
|   handleLivePreviewConnections(); |   chrome.runtime.onConnect.addListener(handleLivePreview); | ||||||
|  | 
 | ||||||
|  |   //#region Public surface
 | ||||||
|  | 
 | ||||||
|  |   // Sorted alphabetically
 | ||||||
|  |   return { | ||||||
| 
 | 
 | ||||||
|   return Object.assign(/** @namespace styleManager */{ |  | ||||||
|     compareRevision, |     compareRevision, | ||||||
|   }, ensurePrepared(/** @namespace styleManager */{ |  | ||||||
|     get, |  | ||||||
|     getByUUID, |  | ||||||
|     getSectionsByUrl, |  | ||||||
|     putByUUID, |  | ||||||
|     installStyle, |  | ||||||
|     deleteStyle, |  | ||||||
|     deleteByUUID, |  | ||||||
|     editSave, |  | ||||||
|     findStyle, |  | ||||||
|     importStyle, |  | ||||||
|     importMany, |  | ||||||
|     toggleStyle, |  | ||||||
|     getAllStyles, // used by import-export
 |  | ||||||
|     getStylesByUrl, // used by popup
 |  | ||||||
|     styleExists, |  | ||||||
|     addExclusion, |  | ||||||
|     removeExclusion, |  | ||||||
|     addInclusion, |  | ||||||
|     removeInclusion, |  | ||||||
|   })); |  | ||||||
| 
 | 
 | ||||||
|   function handleLivePreviewConnections() { |     /** @returns {Promise<number>} style id */ | ||||||
|     chrome.runtime.onConnect.addListener(port => { |     async delete(id, reason) { | ||||||
|       if (port.name !== 'livePreview') { |       await ready; | ||||||
|         return; |       const data = id2data(id); | ||||||
|  |       await db.exec('delete', id); | ||||||
|  |       if (reason !== 'sync') { | ||||||
|  |         API.sync.delete(data.style._id, Date.now()); | ||||||
|       } |       } | ||||||
|       let id; |       for (const url of data.appliesTo) { | ||||||
|       port.onMessage.addListener(data => { |         const cache = cachedStyleForUrl.get(url); | ||||||
|         if (!id) { |         if (cache) delete cache.sections[id]; | ||||||
|           id = data.id; |       } | ||||||
|         } |       dataMap.delete(id); | ||||||
|         const style = styles.get(id); |       uuidIndex.delete(data.style._id); | ||||||
|         style.preview = data; |       await msg.broadcast({ | ||||||
|         broadcastStyleUpdated(style.preview, 'editPreview'); |         method: 'styleDeleted', | ||||||
|  |         style: {id}, | ||||||
|       }); |       }); | ||||||
|       port.onDisconnect.addListener(() => { |       return id; | ||||||
|         port = null; |     }, | ||||||
|         if (id) { | 
 | ||||||
|           const style = styles.get(id); |     /** @returns {Promise<number>} style id */ | ||||||
|           if (!style) { |     async deleteByUUID(_id, rev) { | ||||||
|             // maybe deleted
 |       await ready; | ||||||
|             return; |       const id = uuidIndex.get(_id); | ||||||
|  |       const oldDoc = id && id2style(id); | ||||||
|  |       if (oldDoc && compareRevision(oldDoc._rev, rev) <= 0) { | ||||||
|  |         // FIXME: does it make sense to set reason to 'sync' in deleteByUUID?
 | ||||||
|  |         return API.styles.delete(id, 'sync'); | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  | 
 | ||||||
|  |     /** @returns {Promise<StyleObj>} */ | ||||||
|  |     async editSave(style) { | ||||||
|  |       await ready; | ||||||
|  |       style = mergeWithMapped(style); | ||||||
|  |       style.updateDate = Date.now(); | ||||||
|  |       return handleSave(await saveStyle(style), 'editSave'); | ||||||
|  |     }, | ||||||
|  | 
 | ||||||
|  |     /** @returns {Promise<?StyleObj>} */ | ||||||
|  |     async find(filter) { | ||||||
|  |       await ready; | ||||||
|  |       const filterEntries = Object.entries(filter); | ||||||
|  |       for (const {style} of dataMap.values()) { | ||||||
|  |         if (filterEntries.every(([key, val]) => style[key] === val)) { | ||||||
|  |           return style; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       return null; | ||||||
|  |     }, | ||||||
|  | 
 | ||||||
|  |     /** @returns {Promise<StyleObj[]>} */ | ||||||
|  |     async getAll() { | ||||||
|  |       await ready; | ||||||
|  |       return Array.from(dataMap.values(), data2style); | ||||||
|  |     }, | ||||||
|  | 
 | ||||||
|  |     /** @returns {Promise<StyleObj>} */ | ||||||
|  |     async getByUUID(uuid) { | ||||||
|  |       await ready; | ||||||
|  |       return id2style(uuidIndex.get(uuid)); | ||||||
|  |     }, | ||||||
|  | 
 | ||||||
|  |     /** @returns {Promise<StyleSectionsToApply>} */ | ||||||
|  |     async getSectionsByUrl(url, id, isInitialApply) { | ||||||
|  |       await ready; | ||||||
|  |       let cache = cachedStyleForUrl.get(url); | ||||||
|  |       if (!cache) { | ||||||
|  |         cache = { | ||||||
|  |           sections: {}, | ||||||
|  |           maybeMatch: new Set(), | ||||||
|  |         }; | ||||||
|  |         buildCache(cache, url, dataMap.values()); | ||||||
|  |         cachedStyleForUrl.set(url, cache); | ||||||
|  |       } else if (cache.maybeMatch.size) { | ||||||
|  |         buildCache(cache, url, Array.from(cache.maybeMatch, id2data).filter(Boolean)); | ||||||
|  |       } | ||||||
|  |       const res = id | ||||||
|  |         ? cache.sections[id] ? {[id]: cache.sections[id]} : {} | ||||||
|  |         : cache.sections; | ||||||
|  |       // Avoiding flicker of needlessly applied styles by providing both styles & pref in one API call
 | ||||||
|  |       return isInitialApply && prefs.get('disableAll') | ||||||
|  |         ? Object.assign({disableAll: true}, res) | ||||||
|  |         : res; | ||||||
|  |     }, | ||||||
|  | 
 | ||||||
|  |     /** @returns {Promise<StyleObj>} */ | ||||||
|  |     async get(id) { | ||||||
|  |       await ready; | ||||||
|  |       return id2style(id); | ||||||
|  |     }, | ||||||
|  | 
 | ||||||
|  |     /** @returns {Promise<StylesByUrlResult[]>} */ | ||||||
|  |     async getByUrl(url, id = null) { | ||||||
|  |       await ready; | ||||||
|  |       // FIXME: do we want to cache this? Who would like to open popup rapidly
 | ||||||
|  |       // or search the DB with the same URL?
 | ||||||
|  |       const result = []; | ||||||
|  |       const styles = id | ||||||
|  |         ? [id2style(id)].filter(Boolean) | ||||||
|  |         : Array.from(dataMap.values(), data2style); | ||||||
|  |       const query = createMatchQuery(url); | ||||||
|  |       for (const style of styles) { | ||||||
|  |         let excluded = false; | ||||||
|  |         let sloppy = false; | ||||||
|  |         let sectionMatched = false; | ||||||
|  |         const match = urlMatchStyle(query, style); | ||||||
|  |         // TODO: enable this when the function starts returning false
 | ||||||
|  |         // if (match === false) {
 | ||||||
|  |         // continue;
 | ||||||
|  |         // }
 | ||||||
|  |         if (match === 'excluded') { | ||||||
|  |           excluded = true; | ||||||
|  |         } | ||||||
|  |         for (const section of style.sections) { | ||||||
|  |           if (styleSectionGlobal(section) && styleCodeEmpty(section.code)) { | ||||||
|  |             continue; | ||||||
|           } |           } | ||||||
|           style.preview = null; |           const match = urlMatchSection(query, section); | ||||||
|           broadcastStyleUpdated(style.data, 'editPreviewEnd'); |           if (match) { | ||||||
|         } |             if (match === 'sloppy') { | ||||||
|       }); |               sloppy = true; | ||||||
|     }); |             } | ||||||
|   } |             sectionMatched = true; | ||||||
| 
 |             break; | ||||||
|   function escapeRegExp(text) { |  | ||||||
|     // https://github.com/lodash/lodash/blob/0843bd46ef805dd03c0c8d804630804f3ba0ca3c/lodash.js#L152
 |  | ||||||
|     return text.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&'); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function get(id, noCode = false) { |  | ||||||
|     const data = styles.get(id).data; |  | ||||||
|     return noCode ? getStyleWithNoCode(data) : data; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function getByUUID(uuid) { |  | ||||||
|     const id = uuidIndex.get(uuid); |  | ||||||
|     if (id) { |  | ||||||
|       return get(id); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function getAllStyles() { |  | ||||||
|     return [...styles.values()].map(s => s.data); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function compareRevision(rev1, rev2) { |  | ||||||
|     return rev1 - rev2; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function putByUUID(doc) { |  | ||||||
|     const id = uuidIndex.get(doc._id); |  | ||||||
|     if (id) { |  | ||||||
|       doc.id = id; |  | ||||||
|     } else { |  | ||||||
|       delete doc.id; |  | ||||||
|     } |  | ||||||
|     const oldDoc = id && styles.has(id) && styles.get(id).data; |  | ||||||
|     let diff = -1; |  | ||||||
|     if (oldDoc) { |  | ||||||
|       diff = compareRevision(oldDoc._rev, doc._rev); |  | ||||||
|       if (diff > 0) { |  | ||||||
|         sync.put(oldDoc._id, oldDoc._rev); |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     if (diff < 0) { |  | ||||||
|       return db.exec('put', doc) |  | ||||||
|         .then(event => { |  | ||||||
|           doc.id = event.target.result; |  | ||||||
|           uuidIndex.set(doc._id, doc.id); |  | ||||||
|           return handleSave(doc, 'sync'); |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function toggleStyle(id, enabled) { |  | ||||||
|     const style = styles.get(id); |  | ||||||
|     const data = Object.assign({}, style.data, {enabled}); |  | ||||||
|     return saveStyle(data) |  | ||||||
|       .then(newData => handleSave(newData, 'toggle', false)) |  | ||||||
|       .then(() => id); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   // used by install-hook-userstyles.js
 |  | ||||||
|   function findStyle(filter, noCode = false) { |  | ||||||
|     for (const style of styles.values()) { |  | ||||||
|       if (filterMatch(filter, style.data)) { |  | ||||||
|         return noCode ? getStyleWithNoCode(style.data) : style.data; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     return null; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function styleExists(filter) { |  | ||||||
|     return [...styles.values()].some(s => filterMatch(filter, s.data)); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function filterMatch(filter, target) { |  | ||||||
|     for (const key of Object.keys(filter)) { |  | ||||||
|       if (filter[key] !== target[key]) { |  | ||||||
|         return false; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     return true; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function importStyle(data) { |  | ||||||
|     // FIXME: is it a good idea to save the data directly?
 |  | ||||||
|     return saveStyle(data) |  | ||||||
|       .then(newData => handleSave(newData, 'import')); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function importMany(items) { |  | ||||||
|     items.forEach(beforeSave); |  | ||||||
|     return db.exec('putMany', items) |  | ||||||
|       .then(events => { |  | ||||||
|         for (let i = 0; i < items.length; i++) { |  | ||||||
|           afterSave(items[i], events[i].target.result); |  | ||||||
|         } |  | ||||||
|         return Promise.all(items.map(i => handleSave(i, 'import'))); |  | ||||||
|       }); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function installStyle(data, reason = null) { |  | ||||||
|     const style = styles.get(data.id); |  | ||||||
|     if (!style) { |  | ||||||
|       data = Object.assign(createNewStyle(), data); |  | ||||||
|     } else { |  | ||||||
|       data = Object.assign({}, style.data, data); |  | ||||||
|     } |  | ||||||
|     if (!reason) { |  | ||||||
|       reason = style ? 'update' : 'install'; |  | ||||||
|     } |  | ||||||
|     let url = !data.url && data.updateUrl; |  | ||||||
|     if (url) { |  | ||||||
|       const usoId = URLS.extractUsoArchiveId(url); |  | ||||||
|       url = usoId && `${URLS.usoArchive}?style=${usoId}` || |  | ||||||
|             URLS.extractGreasyForkId(url) && url.match(/^.*?\/\d+/)[0]; |  | ||||||
|       if (url) data.url = data.installationUrl = url; |  | ||||||
|     } |  | ||||||
|     // FIXME: update updateDate? what about usercss config?
 |  | ||||||
|     return calcStyleDigest(data) |  | ||||||
|       .then(digest => { |  | ||||||
|         data.originalDigest = digest; |  | ||||||
|         return saveStyle(data); |  | ||||||
|       }) |  | ||||||
|       .then(newData => handleSave(newData, reason)); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function editSave(data) { |  | ||||||
|     const style = styles.get(data.id); |  | ||||||
|     if (style) { |  | ||||||
|       data = Object.assign({}, style.data, data); |  | ||||||
|     } else { |  | ||||||
|       data = Object.assign(createNewStyle(), data); |  | ||||||
|     } |  | ||||||
|     data.updateDate = Date.now(); |  | ||||||
|     return saveStyle(data) |  | ||||||
|       .then(newData => handleSave(newData, 'editSave')); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function addIncludeExclude(id, rule, type) { |  | ||||||
|     const data = Object.assign({}, styles.get(id).data); |  | ||||||
|     if (!data[type]) { |  | ||||||
|       data[type] = []; |  | ||||||
|     } |  | ||||||
|     if (data[type].includes(rule)) { |  | ||||||
|       throw new Error('The rule already exists'); |  | ||||||
|     } |  | ||||||
|     data[type] = data[type].concat([rule]); |  | ||||||
|     return saveStyle(data) |  | ||||||
|       .then(newData => handleSave(newData, 'styleSettings')); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function removeIncludeExclude(id, rule, type) { |  | ||||||
|     const data = Object.assign({}, styles.get(id).data); |  | ||||||
|     if (!data[type]) { |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|     if (!data[type].includes(rule)) { |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|     data[type] = data[type].filter(r => r !== rule); |  | ||||||
|     return saveStyle(data) |  | ||||||
|       .then(newData => handleSave(newData, 'styleSettings')); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function addExclusion(id, rule) { |  | ||||||
|     return addIncludeExclude(id, rule, 'exclusions'); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function removeExclusion(id, rule) { |  | ||||||
|     return removeIncludeExclude(id, rule, 'exclusions'); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function addInclusion(id, rule) { |  | ||||||
|     return addIncludeExclude(id, rule, 'inclusions'); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function removeInclusion(id, rule) { |  | ||||||
|     return removeIncludeExclude(id, rule, 'inclusions'); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function deleteStyle(id, reason) { |  | ||||||
|     const style = styles.get(id); |  | ||||||
|     const rev = Date.now(); |  | ||||||
|     return db.exec('delete', id) |  | ||||||
|       .then(() => { |  | ||||||
|         if (reason !== 'sync') { |  | ||||||
|           sync.delete(style.data._id, rev); |  | ||||||
|         } |  | ||||||
|         for (const url of style.appliesTo) { |  | ||||||
|           const cache = cachedStyleForUrl.get(url); |  | ||||||
|           if (cache) { |  | ||||||
|             delete cache.sections[id]; |  | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
|         styles.delete(id); |         if (sectionMatched) { | ||||||
|         uuidIndex.delete(style.data._id); |           result.push(/** @namespace StylesByUrlResult */{style, excluded, sloppy}); | ||||||
|         return msg.broadcast({ |         } | ||||||
|           method: 'styleDeleted', |       } | ||||||
|           style: {id}, |       return result; | ||||||
|         }); |     }, | ||||||
|       }) | 
 | ||||||
|       .then(() => id); |     /** @returns {Promise<StyleObj[]>} */ | ||||||
|  |     async importMany(items) { | ||||||
|  |       await ready; | ||||||
|  |       items.forEach(beforeSave); | ||||||
|  |       const events = await db.exec('putMany', items); | ||||||
|  |       return Promise.all(items.map((item, i) => { | ||||||
|  |         afterSave(item, events[i]); | ||||||
|  |         return handleSave(item, 'import'); | ||||||
|  |       })); | ||||||
|  |     }, | ||||||
|  | 
 | ||||||
|  |     /** @returns {Promise<StyleObj>} */ | ||||||
|  |     async import(data) { | ||||||
|  |       await ready; | ||||||
|  |       return handleSave(await saveStyle(data), 'import'); | ||||||
|  |     }, | ||||||
|  | 
 | ||||||
|  |     /** @returns {Promise<StyleObj>} */ | ||||||
|  |     async install(style, reason = null) { | ||||||
|  |       await ready; | ||||||
|  |       reason = reason || dataMap.has(style.id) ? 'update' : 'install'; | ||||||
|  |       style = mergeWithMapped(style); | ||||||
|  |       const url = !style.url && style.updateUrl && ( | ||||||
|  |         URLS.extractUsoArchiveInstallUrl(style.updateUrl) || | ||||||
|  |         URLS.extractGreasyForkInstallUrl(style.updateUrl) | ||||||
|  |       ); | ||||||
|  |       if (url) style.url = style.installationUrl = url; | ||||||
|  |       style.originalDigest = await calcStyleDigest(style); | ||||||
|  |       // FIXME: update updateDate? what about usercss config?
 | ||||||
|  |       return handleSave(await saveStyle(style), reason); | ||||||
|  |     }, | ||||||
|  | 
 | ||||||
|  |     /** @returns {Promise<?StyleObj>} */ | ||||||
|  |     async putByUUID(doc) { | ||||||
|  |       await ready; | ||||||
|  |       const id = uuidIndex.get(doc._id); | ||||||
|  |       if (id) { | ||||||
|  |         doc.id = id; | ||||||
|  |       } else { | ||||||
|  |         delete doc.id; | ||||||
|  |       } | ||||||
|  |       const oldDoc = id && id2style(id); | ||||||
|  |       let diff = -1; | ||||||
|  |       if (oldDoc) { | ||||||
|  |         diff = compareRevision(oldDoc._rev, doc._rev); | ||||||
|  |         if (diff > 0) { | ||||||
|  |           API.sync.put(oldDoc._id, oldDoc._rev); | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       if (diff < 0) { | ||||||
|  |         doc.id = await db.exec('put', doc); | ||||||
|  |         uuidIndex.set(doc._id, doc.id); | ||||||
|  |         return handleSave(doc, 'sync'); | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  | 
 | ||||||
|  |     /** @returns {Promise<number>} style id */ | ||||||
|  |     async toggle(id, enabled) { | ||||||
|  |       await ready; | ||||||
|  |       const style = Object.assign({}, id2style(id), {enabled}); | ||||||
|  |       handleSave(await saveStyle(style), 'toggle', false); | ||||||
|  |       return id; | ||||||
|  |     }, | ||||||
|  | 
 | ||||||
|  |     // using bind() to skip step-into when debugging
 | ||||||
|  | 
 | ||||||
|  |     /** @returns {Promise<StyleObj>} */ | ||||||
|  |     addExclusion: addIncludeExclude.bind(null, 'exclusions'), | ||||||
|  |     /** @returns {Promise<StyleObj>} */ | ||||||
|  |     addInclusion: addIncludeExclude.bind(null, 'inclusions'), | ||||||
|  |     /** @returns {Promise<?StyleObj>} */ | ||||||
|  |     removeExclusion: removeIncludeExclude.bind(null, 'exclusions'), | ||||||
|  |     /** @returns {Promise<?StyleObj>} */ | ||||||
|  |     removeInclusion: removeIncludeExclude.bind(null, 'inclusions'), | ||||||
|  |   }; | ||||||
|  |   //#endregion
 | ||||||
|  | 
 | ||||||
|  |   //#region Implementation
 | ||||||
|  | 
 | ||||||
|  |   /** @returns {StyleMapData} */ | ||||||
|  |   function id2data(id) { | ||||||
|  |     return dataMap.get(id); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   function deleteByUUID(_id, rev) { |   /** @returns {?StyleObj} */ | ||||||
|     const id = uuidIndex.get(_id); |   function id2style(id) { | ||||||
|     const oldDoc = id && styles.has(id) && styles.get(id).data; |     return (dataMap.get(id) || {}).style; | ||||||
|     if (oldDoc && compareRevision(oldDoc._rev, rev) <= 0) { |  | ||||||
|       // FIXME: does it make sense to set reason to 'sync' in deleteByUUID?
 |  | ||||||
|       return deleteStyle(id, 'sync'); |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   function ensurePrepared(methods) { |   /** @returns {?StyleObj} */ | ||||||
|     const prepared = {}; |   function data2style(data) { | ||||||
|     for (const [name, fn] of Object.entries(methods)) { |     return data && data.style; | ||||||
|       prepared[name] = (...args) => |  | ||||||
|         preparing.then(() => fn(...args)); |  | ||||||
|     } |  | ||||||
|     return prepared; |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   /** @returns {StyleObj} */ | ||||||
|   function createNewStyle() { |   function createNewStyle() { | ||||||
|     return { |     return /** @namespace StyleObj */{ | ||||||
|       enabled: true, |       enabled: true, | ||||||
|       updateUrl: null, |       updateUrl: null, | ||||||
|       md5Url: null, |       md5Url: null, | ||||||
|  | @ -352,43 +323,105 @@ const styleManager = (() => { | ||||||
|     }; |     }; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   function broadcastStyleUpdated(data, reason, method = 'styleUpdated', codeIsUpdated = true) { |   /** @returns {void} */ | ||||||
|     const style = styles.get(data.id); |   function storeInMap(style) { | ||||||
|  |     dataMap.set(style.id, { | ||||||
|  |       style, | ||||||
|  |       appliesTo: new Set(), | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** @returns {StyleObj} */ | ||||||
|  |   function mergeWithMapped(style) { | ||||||
|  |     return Object.assign({}, | ||||||
|  |       id2style(style.id) || createNewStyle(), | ||||||
|  |       style); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   function handleLivePreview(port) { | ||||||
|  |     if (port.name !== 'livePreview') { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     let id; | ||||||
|  |     port.onMessage.addListener(style => { | ||||||
|  |       if (!id) id = style.id; | ||||||
|  |       const data = id2data(id); | ||||||
|  |       data.preview = style; | ||||||
|  |       broadcastStyleUpdated(style, 'editPreview'); | ||||||
|  |     }); | ||||||
|  |     port.onDisconnect.addListener(() => { | ||||||
|  |       port = null; | ||||||
|  |       if (id) { | ||||||
|  |         const data = id2data(id); | ||||||
|  |         if (data) { | ||||||
|  |           data.preview = null; | ||||||
|  |           broadcastStyleUpdated(data.style, 'editPreviewEnd'); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   function compareRevision(rev1, rev2) { | ||||||
|  |     return rev1 - rev2; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async function addIncludeExclude(type, id, rule) { | ||||||
|  |     await ready; | ||||||
|  |     const style = Object.assign({}, id2style(id)); | ||||||
|  |     const list = style[type] || (style[type] = []); | ||||||
|  |     if (list.includes(rule)) { | ||||||
|  |       throw new Error('The rule already exists'); | ||||||
|  |     } | ||||||
|  |     style[type] = list.concat([rule]); | ||||||
|  |     return handleSave(await saveStyle(style), 'styleSettings'); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async function removeIncludeExclude(type, id, rule) { | ||||||
|  |     await ready; | ||||||
|  |     const style = Object.assign({}, id2style(id)); | ||||||
|  |     const list = style[type]; | ||||||
|  |     if (!list || !list.includes(rule)) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     style[type] = list.filter(r => r !== rule); | ||||||
|  |     return handleSave(await saveStyle(style), 'styleSettings'); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   function broadcastStyleUpdated(style, reason, method = 'styleUpdated', codeIsUpdated = true) { | ||||||
|  |     const {id} = style; | ||||||
|  |     const data = id2data(id); | ||||||
|     const excluded = new Set(); |     const excluded = new Set(); | ||||||
|     const updated = new Set(); |     const updated = new Set(); | ||||||
|     for (const [url, cache] of cachedStyleForUrl.entries()) { |     for (const [url, cache] of cachedStyleForUrl.entries()) { | ||||||
|       if (!style.appliesTo.has(url)) { |       if (!data.appliesTo.has(url)) { | ||||||
|         cache.maybeMatch.add(data.id); |         cache.maybeMatch.add(id); | ||||||
|         continue; |         continue; | ||||||
|       } |       } | ||||||
|       const code = getAppliedCode(createMatchQuery(url), data); |       const code = getAppliedCode(createMatchQuery(url), style); | ||||||
|       if (!code) { |       if (code) { | ||||||
|         excluded.add(url); |  | ||||||
|         delete cache.sections[data.id]; |  | ||||||
|       } else { |  | ||||||
|         updated.add(url); |         updated.add(url); | ||||||
|         cache.sections[data.id] = { |         cache.sections[id] = {id, code}; | ||||||
|           id: data.id, |       } else { | ||||||
|           code, |         excluded.add(url); | ||||||
|         }; |         delete cache.sections[id]; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|     style.appliesTo = updated; |     data.appliesTo = updated; | ||||||
|     return msg.broadcast({ |     return msg.broadcast({ | ||||||
|       method, |       method, | ||||||
|       style: { |  | ||||||
|         id: data.id, |  | ||||||
|         md5Url: data.md5Url, |  | ||||||
|         enabled: data.enabled, |  | ||||||
|       }, |  | ||||||
|       reason, |       reason, | ||||||
|       codeIsUpdated, |       codeIsUpdated, | ||||||
|  |       style: { | ||||||
|  |         id, | ||||||
|  |         md5Url: style.md5Url, | ||||||
|  |         enabled: style.enabled, | ||||||
|  |       }, | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   function beforeSave(style) { |   function beforeSave(style) { | ||||||
|     if (!style.name) { |     if (!style.name) { | ||||||
|       throw new Error('style name is empty'); |       throw new Error('Style name is empty'); | ||||||
|     } |     } | ||||||
|     for (const key of DELETE_IF_NULL) { |     for (const key of DELETE_IF_NULL) { | ||||||
|       if (style[key] == null) { |       if (style[key] == null) { | ||||||
|  | @ -407,114 +440,29 @@ const styleManager = (() => { | ||||||
|       style.id = newId; |       style.id = newId; | ||||||
|     } |     } | ||||||
|     uuidIndex.set(style._id, style.id); |     uuidIndex.set(style._id, style.id); | ||||||
|     sync.put(style._id, style._rev); |     API.sync.put(style._id, style._rev); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   function saveStyle(style) { |   async function saveStyle(style) { | ||||||
|     beforeSave(style); |     beforeSave(style); | ||||||
|     return db.exec('put', style) |     const newId = await db.exec('put', style); | ||||||
|       .then(event => { |     afterSave(style, newId); | ||||||
|         afterSave(style, event.target.result); |     return style; | ||||||
|         return style; |  | ||||||
|       }); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   function handleSave(data, reason, codeIsUpdated) { |   function handleSave(style, reason, codeIsUpdated) { | ||||||
|     const style = styles.get(data.id); |     const data = id2data(style.id); | ||||||
|     let method; |     const method = data ? 'styleUpdated' : 'styleAdded'; | ||||||
|     if (!style) { |     if (!data) { | ||||||
|       styles.set(data.id, { |       storeInMap(style); | ||||||
|         appliesTo: new Set(), |  | ||||||
|         data, |  | ||||||
|       }); |  | ||||||
|       method = 'styleAdded'; |  | ||||||
|     } else { |     } else { | ||||||
|       style.data = data; |       data.style = style; | ||||||
|       method = 'styleUpdated'; |  | ||||||
|     } |     } | ||||||
|     broadcastStyleUpdated(data, reason, method, codeIsUpdated); |     broadcastStyleUpdated(style, reason, method, codeIsUpdated); | ||||||
|     return data; |     return style; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   // get styles matching a URL, including sloppy regexps and excluded items.
 |   // get styles matching a URL, including sloppy regexps and excluded items.
 | ||||||
|   function getStylesByUrl(url, id = null) { |  | ||||||
|     // FIXME: do we want to cache this? Who would like to open popup rapidly
 |  | ||||||
|     // or search the DB with the same URL?
 |  | ||||||
|     const result = []; |  | ||||||
|     const datas = !id ? [...styles.values()].map(s => s.data) : |  | ||||||
|       styles.has(id) ? [styles.get(id).data] : []; |  | ||||||
|     const query = createMatchQuery(url); |  | ||||||
|     for (const data of datas) { |  | ||||||
|       let excluded = false; |  | ||||||
|       let sloppy = false; |  | ||||||
|       let sectionMatched = false; |  | ||||||
|       const match = urlMatchStyle(query, data); |  | ||||||
|       // TODO: enable this when the function starts returning false
 |  | ||||||
|       // if (match === false) {
 |  | ||||||
|         // continue;
 |  | ||||||
|       // }
 |  | ||||||
|       if (match === 'excluded') { |  | ||||||
|         excluded = true; |  | ||||||
|       } |  | ||||||
|       for (const section of data.sections) { |  | ||||||
|         if (styleSectionGlobal(section) && styleCodeEmpty(section.code)) { |  | ||||||
|           continue; |  | ||||||
|         } |  | ||||||
|         const match = urlMatchSection(query, section); |  | ||||||
|         if (match) { |  | ||||||
|           if (match === 'sloppy') { |  | ||||||
|             sloppy = true; |  | ||||||
|           } |  | ||||||
|           sectionMatched = true; |  | ||||||
|           break; |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       if (sectionMatched) { |  | ||||||
|         result.push({data, excluded, sloppy}); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     return result; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function getSectionsByUrl(url, id, isInitialApply) { |  | ||||||
|     let cache = cachedStyleForUrl.get(url); |  | ||||||
|     if (!cache) { |  | ||||||
|       cache = { |  | ||||||
|         sections: {}, |  | ||||||
|         maybeMatch: new Set(), |  | ||||||
|       }; |  | ||||||
|       buildCache(styles.values()); |  | ||||||
|       cachedStyleForUrl.set(url, cache); |  | ||||||
|     } else if (cache.maybeMatch.size) { |  | ||||||
|       buildCache( |  | ||||||
|         [...cache.maybeMatch] |  | ||||||
|           .filter(i => styles.has(i)) |  | ||||||
|           .map(i => styles.get(i)) |  | ||||||
|       ); |  | ||||||
|     } |  | ||||||
|     const res = id |  | ||||||
|       ? cache.sections[id] ? {[id]: cache.sections[id]} : {} |  | ||||||
|       : cache.sections; |  | ||||||
|     // Avoiding flicker of needlessly applied styles by providing both styles & pref in one API call
 |  | ||||||
|     return isInitialApply && prefs.get('disableAll') |  | ||||||
|       ? Object.assign({disableAll: true}, res) |  | ||||||
|       : res; |  | ||||||
| 
 |  | ||||||
|     function buildCache(styleList) { |  | ||||||
|       const query = createMatchQuery(url); |  | ||||||
|       for (const {appliesTo, data, preview} of styleList) { |  | ||||||
|         const code = getAppliedCode(query, preview || data); |  | ||||||
|         if (code) { |  | ||||||
|           cache.sections[data.id] = { |  | ||||||
|             id: data.id, |  | ||||||
|             code, |  | ||||||
|           }; |  | ||||||
|           appliesTo.add(url); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function getAppliedCode(query, data) { |   function getAppliedCode(query, data) { | ||||||
|     if (urlMatchStyle(query, data) !== true) { |     if (urlMatchStyle(query, data) !== true) { | ||||||
|       return; |       return; | ||||||
|  | @ -528,60 +476,45 @@ const styleManager = (() => { | ||||||
|     return code.length && code; |     return code.length && code; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   function prepare() { |   async function init() { | ||||||
|     const ADD_MISSING_PROPS = { |     const styles = await db.exec('getAll') || []; | ||||||
|       name: style => `ID: ${style.id}`, |     const updated = styles.filter(style => | ||||||
|       _id: () => uuidv4(), |       addMissingProps(style) + | ||||||
|       _rev: () => Date.now(), |       addCustomName(style)); | ||||||
|     }; |     if (updated.length) { | ||||||
| 
 |       await db.exec('putMany', updated); | ||||||
|     return db.exec('getAll') |  | ||||||
|       .then(event => event.target.result || []) |  | ||||||
|       .then(styleList => { |  | ||||||
|         // setup missing _id, _rev
 |  | ||||||
|         const updated = []; |  | ||||||
|         for (const style of styleList) { |  | ||||||
|           if (addMissingProperties(style)) { |  | ||||||
|             updated.push(style); |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|         if (updated.length) { |  | ||||||
|           return db.exec('putMany', updated) |  | ||||||
|             .then(() => styleList); |  | ||||||
|         } |  | ||||||
|         return styleList; |  | ||||||
|       }) |  | ||||||
|       .then(styleList => { |  | ||||||
|         for (const style of styleList) { |  | ||||||
|           fixUsoMd5Issue(style); |  | ||||||
|           styles.set(style.id, { |  | ||||||
|             appliesTo: new Set(), |  | ||||||
|             data: style, |  | ||||||
|           }); |  | ||||||
|           uuidIndex.set(style._id, style.id); |  | ||||||
|         } |  | ||||||
|       }); |  | ||||||
| 
 |  | ||||||
|     function addMissingProperties(style) { |  | ||||||
|       let touched = false; |  | ||||||
|       for (const key in ADD_MISSING_PROPS) { |  | ||||||
|         if (!style[key]) { |  | ||||||
|           style[key] = ADD_MISSING_PROPS[key](style); |  | ||||||
|           touched = true; |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       // upgrade the old way of customizing local names
 |  | ||||||
|       const {originalName} = style; |  | ||||||
|       if (originalName) { |  | ||||||
|         touched = true; |  | ||||||
|         if (originalName !== style.name) { |  | ||||||
|           style.customName = style.name; |  | ||||||
|           style.name = originalName; |  | ||||||
|         } |  | ||||||
|         delete style.originalName; |  | ||||||
|       } |  | ||||||
|       return touched; |  | ||||||
|     } |     } | ||||||
|  |     for (const style of styles) { | ||||||
|  |       fixUsoMd5Issue(style); | ||||||
|  |       storeInMap(style); | ||||||
|  |       uuidIndex.set(style._id, style.id); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   function addMissingProps(style) { | ||||||
|  |     let res = 0; | ||||||
|  |     for (const key in MISSING_PROPS) { | ||||||
|  |       if (!style[key]) { | ||||||
|  |         style[key] = MISSING_PROPS[key](style); | ||||||
|  |         res = 1; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     return res; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** Upgrades the old way of customizing local names */ | ||||||
|  |   function addCustomName(style) { | ||||||
|  |     let res = 0; | ||||||
|  |     const {originalName} = style; | ||||||
|  |     if (originalName) { | ||||||
|  |       res = 1; | ||||||
|  |       if (originalName !== style.name) { | ||||||
|  |         style.customName = style.name; | ||||||
|  |         style.name = originalName; | ||||||
|  |       } | ||||||
|  |       delete style.originalName; | ||||||
|  |     } | ||||||
|  |     return res; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   function urlMatchStyle(query, style) { |   function urlMatchStyle(query, style) { | ||||||
|  | @ -652,7 +585,8 @@ const styleManager = (() => { | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   function compileGlob(text) { |   function compileGlob(text) { | ||||||
|     return escapeRegExp(text).replace(/\\\\\\\*|\\\*/g, m => m.length > 2 ? m : '.*'); |     return stringAsRegExp(text, '', true) | ||||||
|  |       .replace(/\\\\\\\*|\\\*/g, m => m.length > 2 ? m : '.*'); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   function buildExclusion(text) { |   function buildExclusion(text) { | ||||||
|  | @ -706,6 +640,18 @@ const styleManager = (() => { | ||||||
|     }; |     }; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   function buildCache(cache, url, styleList) { | ||||||
|  |     const query = createMatchQuery(url); | ||||||
|  |     for (const {style, appliesTo, preview} of styleList) { | ||||||
|  |       const code = getAppliedCode(query, preview || style); | ||||||
|  |       if (code) { | ||||||
|  |         const id = style.id; | ||||||
|  |         cache.sections[id] = {id, code}; | ||||||
|  |         appliesTo.add(url); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   function createURL(url) { |   function createURL(url) { | ||||||
|     try { |     try { | ||||||
|       return new URL(url); |       return new URL(url); | ||||||
|  | @ -726,4 +672,5 @@ const styleManager = (() => { | ||||||
|   function hex4dashed(num, i) { |   function hex4dashed(num, i) { | ||||||
|     return (num + 0x10000).toString(16).slice(-4) + (i >= 1 && i <= 4 ? '-' : ''); |     return (num + 0x10000).toString(16).slice(-4) + (i >= 1 && i <= 4 ? '-' : ''); | ||||||
|   } |   } | ||||||
|  |   //#endregion
 | ||||||
| })(); | })(); | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| /* global API_METHODS styleManager CHROME prefs */ | /* global API CHROME prefs */ | ||||||
| 'use strict'; | 'use strict'; | ||||||
| 
 | 
 | ||||||
| API_METHODS.styleViaAPI = !CHROME && (() => { | API.styleViaAPI = !CHROME && (() => { | ||||||
|   const ACTIONS = { |   const ACTIONS = { | ||||||
|     styleApply, |     styleApply, | ||||||
|     styleDeleted, |     styleDeleted, | ||||||
|  | @ -37,7 +37,7 @@ API_METHODS.styleViaAPI = !CHROME && (() => { | ||||||
|       throw new Error('we do not count styles for frames'); |       throw new Error('we do not count styles for frames'); | ||||||
|     } |     } | ||||||
|     const {frameStyles} = getCachedData(tab.id, frameId); |     const {frameStyles} = getCachedData(tab.id, frameId); | ||||||
|     API_METHODS.updateIconBadge.call({sender}, Object.keys(frameStyles)); |     API.updateIconBadge.call({sender}, Object.keys(frameStyles)); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   function styleApply({id = null, ignoreUrlCheck = false}, {tab, frameId, url}) { |   function styleApply({id = null, ignoreUrlCheck = false}, {tab, frameId, url}) { | ||||||
|  | @ -48,7 +48,7 @@ API_METHODS.styleViaAPI = !CHROME && (() => { | ||||||
|     if (id === null && !ignoreUrlCheck && frameStyles.url === url) { |     if (id === null && !ignoreUrlCheck && frameStyles.url === url) { | ||||||
|       return NOP; |       return NOP; | ||||||
|     } |     } | ||||||
|     return styleManager.getSectionsByUrl(url, id).then(sections => { |     return API.styles.getSectionsByUrl(url, id).then(sections => { | ||||||
|       const tasks = []; |       const tasks = []; | ||||||
|       for (const section of Object.values(sections)) { |       for (const section of Object.values(sections)) { | ||||||
|         const styleId = section.id; |         const styleId = section.id; | ||||||
|  |  | ||||||
|  | @ -1,4 +1,8 @@ | ||||||
| /* global API CHROME prefs */ | /* global | ||||||
|  |   API | ||||||
|  |   CHROME | ||||||
|  |   prefs | ||||||
|  | */ | ||||||
| 'use strict'; | 'use strict'; | ||||||
| 
 | 
 | ||||||
| // eslint-disable-next-line no-unused-expressions
 | // eslint-disable-next-line no-unused-expressions
 | ||||||
|  | @ -67,14 +71,14 @@ CHROME && (async () => { | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /** @param {chrome.webRequest.WebRequestBodyDetails} req */ |   /** @param {chrome.webRequest.WebRequestBodyDetails} req */ | ||||||
|   function prepareStyles(req) { |   async function prepareStyles(req) { | ||||||
|     API.getSectionsByUrl(req.url).then(sections => { |     const sections = await API.styles.getSectionsByUrl(req.url); | ||||||
|       if (Object.keys(sections).length) { |     if (Object.keys(sections).length) { | ||||||
|         stylesToPass[req.requestId] = !enabled.xhr ? true : |       stylesToPass[req.requestId] = !enabled.xhr ? true : | ||||||
|           URL.createObjectURL(new Blob([JSON.stringify(sections)])).slice(blobUrlPrefix.length); |         URL.createObjectURL(new Blob([JSON.stringify(sections)])) | ||||||
|         setTimeout(cleanUp, 600e3, req.requestId); |           .slice(blobUrlPrefix.length); | ||||||
|       } |       setTimeout(cleanUp, 600e3, req.requestId); | ||||||
|     }); |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /** @param {chrome.webRequest.WebResponseHeadersDetails} req */ |   /** @param {chrome.webRequest.WebResponseHeadersDetails} req */ | ||||||
|  |  | ||||||
|  | @ -1,13 +1,23 @@ | ||||||
| /* global dbToCloud styleManager chromeLocal prefs tokenManager msg */ | /* global | ||||||
|  |   API | ||||||
|  |   chromeLocal | ||||||
|  |   dbToCloud | ||||||
|  |   msg | ||||||
|  |   prefs | ||||||
|  |   styleManager | ||||||
|  |   tokenManager | ||||||
|  | */ | ||||||
| /* exported sync */ | /* exported sync */ | ||||||
| 
 | 
 | ||||||
| 'use strict'; | 'use strict'; | ||||||
| 
 | 
 | ||||||
| const sync = (() => { | const sync = API.sync = (() => { | ||||||
|   const SYNC_DELAY = 1; // minutes
 |   const SYNC_DELAY = 1; // minutes
 | ||||||
|   const SYNC_INTERVAL = 30; // minutes
 |   const SYNC_INTERVAL = 30; // minutes
 | ||||||
| 
 | 
 | ||||||
|  |   /** @typedef API.sync.Status */ | ||||||
|   const status = { |   const status = { | ||||||
|  |     /** @type {'connected'|'connecting'|'disconnected'|'disconnecting'} */ | ||||||
|     state: 'disconnected', |     state: 'disconnected', | ||||||
|     syncing: false, |     syncing: false, | ||||||
|     progress: null, |     progress: null, | ||||||
|  | @ -18,21 +28,30 @@ const sync = (() => { | ||||||
|   let currentDrive; |   let currentDrive; | ||||||
|   const ctrl = dbToCloud.dbToCloud({ |   const ctrl = dbToCloud.dbToCloud({ | ||||||
|     onGet(id) { |     onGet(id) { | ||||||
|       return styleManager.getByUUID(id); |       return API.styles.getByUUID(id); | ||||||
|     }, |     }, | ||||||
|     onPut(doc) { |     onPut(doc) { | ||||||
|       return styleManager.putByUUID(doc); |       return API.styles.putByUUID(doc); | ||||||
|     }, |     }, | ||||||
|     onDelete(id, rev) { |     onDelete(id, rev) { | ||||||
|       return styleManager.deleteByUUID(id, rev); |       return API.styles.deleteByUUID(id, rev); | ||||||
|     }, |     }, | ||||||
|     onFirstSync() { |     async onFirstSync() { | ||||||
|       return styleManager.getAllStyles() |       for (const i of await API.styles.getAll()) { | ||||||
|         .then(styles => { |         ctrl.put(i._id, i._rev); | ||||||
|           styles.forEach(i => ctrl.put(i._id, i._rev)); |       } | ||||||
|         }); |     }, | ||||||
|  |     onProgress(e) { | ||||||
|  |       if (e.phase === 'start') { | ||||||
|  |         status.syncing = true; | ||||||
|  |       } else if (e.phase === 'end') { | ||||||
|  |         status.syncing = false; | ||||||
|  |         status.progress = null; | ||||||
|  |       } else { | ||||||
|  |         status.progress = e; | ||||||
|  |       } | ||||||
|  |       emitStatusChange(); | ||||||
|     }, |     }, | ||||||
|     onProgress, |  | ||||||
|     compareRevision(a, b) { |     compareRevision(a, b) { | ||||||
|       return styleManager.compareRevision(a, b); |       return styleManager.compareRevision(a, b); | ||||||
|     }, |     }, | ||||||
|  | @ -46,55 +65,126 @@ const sync = (() => { | ||||||
|     }, |     }, | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   const initializing = prefs.initializing.then(() => { |   const ready = prefs.initializing.then(() => { | ||||||
|     prefs.subscribe(['sync.enabled'], onPrefChange); |     prefs.subscribe('sync.enabled', | ||||||
|     onPrefChange(null, prefs.get('sync.enabled')); |       (_, val) => val === 'none' | ||||||
|  |         ? sync.stop() | ||||||
|  |         : sync.start(val, true), | ||||||
|  |       {now: true}); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   chrome.alarms.onAlarm.addListener(info => { |   chrome.alarms.onAlarm.addListener(info => { | ||||||
|     if (info.name === 'syncNow') { |     if (info.name === 'syncNow') { | ||||||
|       syncNow().catch(console.error); |       sync.syncNow(); | ||||||
|     } |     } | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   return Object.assign({ |   // Sorted alphabetically
 | ||||||
|     getStatus: () => status, |   return { | ||||||
|   }, ensurePrepared({ | 
 | ||||||
|     start, |     async delete(...args) { | ||||||
|     stop, |       await ready; | ||||||
|     put: (...args) => { |  | ||||||
|       if (!currentDrive) return; |  | ||||||
|       schedule(); |  | ||||||
|       return ctrl.put(...args); |  | ||||||
|     }, |  | ||||||
|     delete: (...args) => { |  | ||||||
|       if (!currentDrive) return; |       if (!currentDrive) return; | ||||||
|       schedule(); |       schedule(); | ||||||
|       return ctrl.delete(...args); |       return ctrl.delete(...args); | ||||||
|     }, |     }, | ||||||
|     syncNow, |  | ||||||
|     login, |  | ||||||
|   })); |  | ||||||
| 
 | 
 | ||||||
|   function ensurePrepared(obj) { |     /** | ||||||
|     return Object.entries(obj).reduce((o, [key, fn]) => { |      * @returns {Promise<API.sync.Status>} | ||||||
|       o[key] = (...args) => |      */ | ||||||
|         initializing.then(() => fn(...args)); |     async getStatus() { | ||||||
|       return o; |       return status; | ||||||
|     }, {}); |     }, | ||||||
|   } |  | ||||||
| 
 | 
 | ||||||
|   function onProgress(e) { |     async login(name = prefs.get('sync.enabled')) { | ||||||
|     if (e.phase === 'start') { |       await ready; | ||||||
|       status.syncing = true; |       try { | ||||||
|     } else if (e.phase === 'end') { |         await tokenManager.getToken(name, true); | ||||||
|       status.syncing = false; |       } catch (err) { | ||||||
|       status.progress = null; |         if (/Authorization page could not be loaded/i.test(err.message)) { | ||||||
|     } else { |           // FIXME: Chrome always fails at the first login so we try again
 | ||||||
|       status.progress = e; |           await tokenManager.getToken(name); | ||||||
|     } |         } | ||||||
|     emitStatusChange(); |         throw err; | ||||||
|   } |       } | ||||||
|  |       status.login = true; | ||||||
|  |       emitStatusChange(); | ||||||
|  |     }, | ||||||
|  | 
 | ||||||
|  |     async put(...args) { | ||||||
|  |       await ready; | ||||||
|  |       if (!currentDrive) return; | ||||||
|  |       schedule(); | ||||||
|  |       return ctrl.put(...args); | ||||||
|  |     }, | ||||||
|  | 
 | ||||||
|  |     async start(name, fromPref = false) { | ||||||
|  |       await ready; | ||||||
|  |       if (currentDrive) { | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |       currentDrive = getDrive(name); | ||||||
|  |       ctrl.use(currentDrive); | ||||||
|  |       status.state = 'connecting'; | ||||||
|  |       status.currentDriveName = currentDrive.name; | ||||||
|  |       status.login = true; | ||||||
|  |       emitStatusChange(); | ||||||
|  |       try { | ||||||
|  |         if (!fromPref) { | ||||||
|  |           await sync.login(name).catch(handle401Error); | ||||||
|  |         } | ||||||
|  |         await sync.syncNow(); | ||||||
|  |         status.errorMessage = null; | ||||||
|  |       } catch (err) { | ||||||
|  |         status.errorMessage = err.message; | ||||||
|  |         // FIXME: should we move this logic to options.js?
 | ||||||
|  |         if (!fromPref) { | ||||||
|  |           console.error(err); | ||||||
|  |           return sync.stop(); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       prefs.set('sync.enabled', name); | ||||||
|  |       status.state = 'connected'; | ||||||
|  |       schedule(SYNC_INTERVAL); | ||||||
|  |       emitStatusChange(); | ||||||
|  |     }, | ||||||
|  | 
 | ||||||
|  |     async stop() { | ||||||
|  |       await ready; | ||||||
|  |       if (!currentDrive) { | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |       chrome.alarms.clear('syncNow'); | ||||||
|  |       status.state = 'disconnecting'; | ||||||
|  |       emitStatusChange(); | ||||||
|  |       try { | ||||||
|  |         await ctrl.stop(); | ||||||
|  |         await tokenManager.revokeToken(currentDrive.name); | ||||||
|  |         await chromeLocal.remove(`sync/state/${currentDrive.name}`); | ||||||
|  |       } catch (e) { | ||||||
|  |       } | ||||||
|  |       currentDrive = null; | ||||||
|  |       prefs.set('sync.enabled', 'none'); | ||||||
|  |       status.state = 'disconnected'; | ||||||
|  |       status.currentDriveName = null; | ||||||
|  |       status.login = false; | ||||||
|  |       emitStatusChange(); | ||||||
|  |     }, | ||||||
|  | 
 | ||||||
|  |     async syncNow() { | ||||||
|  |       await ready; | ||||||
|  |       if (!currentDrive) { | ||||||
|  |         return Promise.reject(new Error('cannot sync when disconnected')); | ||||||
|  |       } | ||||||
|  |       try { | ||||||
|  |         await (ctrl.isInit() ? ctrl.syncNow() : ctrl.start()).catch(handle401Error); | ||||||
|  |         status.errorMessage = null; | ||||||
|  |       } catch (err) { | ||||||
|  |         status.errorMessage = err.message; | ||||||
|  |       } | ||||||
|  |       emitStatusChange(); | ||||||
|  |     }, | ||||||
|  |   }; | ||||||
| 
 | 
 | ||||||
|   function schedule(delay = SYNC_DELAY) { |   function schedule(delay = SYNC_DELAY) { | ||||||
|     chrome.alarms.create('syncNow', { |     chrome.alarms.create('syncNow', { | ||||||
|  | @ -103,106 +193,25 @@ const sync = (() => { | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   function onPrefChange(key, value) { |   async function handle401Error(err) { | ||||||
|     if (value === 'none') { |     let emit; | ||||||
|       stop().catch(console.error); |  | ||||||
|     } else { |  | ||||||
|       start(value, true).catch(console.error); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function withFinally(p, cleanup) { |  | ||||||
|     return p.then( |  | ||||||
|       result => { |  | ||||||
|         cleanup(undefined, result); |  | ||||||
|         return result; |  | ||||||
|       }, |  | ||||||
|       err => { |  | ||||||
|         cleanup(err); |  | ||||||
|         throw err; |  | ||||||
|       } |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function syncNow() { |  | ||||||
|     if (!currentDrive) { |  | ||||||
|       return Promise.reject(new Error('cannot sync when disconnected')); |  | ||||||
|     } |  | ||||||
|     return withFinally( |  | ||||||
|       (ctrl.isInit() ? ctrl.syncNow() : ctrl.start()) |  | ||||||
|         .catch(handle401Error), |  | ||||||
|       err => { |  | ||||||
|         status.errorMessage = err ? err.message : null; |  | ||||||
|         emitStatusChange(); |  | ||||||
|       } |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function handle401Error(err) { |  | ||||||
|     if (err.code === 401) { |     if (err.code === 401) { | ||||||
|       return tokenManager.revokeToken(currentDrive.name) |       await tokenManager.revokeToken(currentDrive.name).catch(console.error); | ||||||
|         .catch(console.error) |       emit = true; | ||||||
|         .then(() => { |     } else if (/User interaction required|Requires user interaction/i.test(err.message)) { | ||||||
|           status.login = false; |       emit = true; | ||||||
|           emitStatusChange(); |  | ||||||
|           throw err; |  | ||||||
|         }); |  | ||||||
|     } |     } | ||||||
|     if (/User interaction required|Requires user interaction/i.test(err.message)) { |     if (emit) { | ||||||
|       status.login = false; |       status.login = false; | ||||||
|       emitStatusChange(); |       emitStatusChange(); | ||||||
|     } |     } | ||||||
|     throw err; |     return Promise.reject(err); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   function emitStatusChange() { |   function emitStatusChange() { | ||||||
|     msg.broadcastExtension({method: 'syncStatusUpdate', status}); |     msg.broadcastExtension({method: 'syncStatusUpdate', status}); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   function login(name = prefs.get('sync.enabled')) { |  | ||||||
|     return tokenManager.getToken(name, true) |  | ||||||
|       .catch(err => { |  | ||||||
|         if (/Authorization page could not be loaded/i.test(err.message)) { |  | ||||||
|           // FIXME: Chrome always fails at the first login so we try again
 |  | ||||||
|           return tokenManager.getToken(name); |  | ||||||
|         } |  | ||||||
|         throw err; |  | ||||||
|       }) |  | ||||||
|       .then(() => { |  | ||||||
|         status.login = true; |  | ||||||
|         emitStatusChange(); |  | ||||||
|       }); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function start(name, fromPref = false) { |  | ||||||
|     if (currentDrive) { |  | ||||||
|       return Promise.resolve(); |  | ||||||
|     } |  | ||||||
|     currentDrive = getDrive(name); |  | ||||||
|     ctrl.use(currentDrive); |  | ||||||
|     status.state = 'connecting'; |  | ||||||
|     status.currentDriveName = currentDrive.name; |  | ||||||
|     status.login = true; |  | ||||||
|     emitStatusChange(); |  | ||||||
|     return withFinally( |  | ||||||
|       (fromPref ? Promise.resolve() : login(name)) |  | ||||||
|         .catch(handle401Error) |  | ||||||
|         .then(() => syncNow()), |  | ||||||
|       err => { |  | ||||||
|         status.errorMessage = err ? err.message : null; |  | ||||||
|         // FIXME: should we move this logic to options.js?
 |  | ||||||
|         if (err && !fromPref) { |  | ||||||
|           console.error(err); |  | ||||||
|           return stop(); |  | ||||||
|         } |  | ||||||
|         prefs.set('sync.enabled', name); |  | ||||||
|         schedule(SYNC_INTERVAL); |  | ||||||
|         status.state = 'connected'; |  | ||||||
|         emitStatusChange(); |  | ||||||
|       } |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function getDrive(name) { |   function getDrive(name) { | ||||||
|     if (name === 'dropbox' || name === 'google' || name === 'onedrive') { |     if (name === 'dropbox' || name === 'google' || name === 'onedrive') { | ||||||
|       return dbToCloud.drive[name]({ |       return dbToCloud.drive[name]({ | ||||||
|  | @ -211,26 +220,4 @@ const sync = (() => { | ||||||
|     } |     } | ||||||
|     throw new Error(`unknown cloud name: ${name}`); |     throw new Error(`unknown cloud name: ${name}`); | ||||||
|   } |   } | ||||||
| 
 |  | ||||||
|   function stop() { |  | ||||||
|     if (!currentDrive) { |  | ||||||
|       return Promise.resolve(); |  | ||||||
|     } |  | ||||||
|     chrome.alarms.clear('syncNow'); |  | ||||||
|     status.state = 'disconnecting'; |  | ||||||
|     emitStatusChange(); |  | ||||||
|     return withFinally( |  | ||||||
|       ctrl.stop() |  | ||||||
|         .then(() => tokenManager.revokeToken(currentDrive.name)) |  | ||||||
|         .then(() => chromeLocal.remove(`sync/state/${currentDrive.name}`)), |  | ||||||
|       () => { |  | ||||||
|         currentDrive = null; |  | ||||||
|         prefs.set('sync.enabled', 'none'); |  | ||||||
|         status.state = 'disconnected'; |  | ||||||
|         status.currentDriveName = null; |  | ||||||
|         status.login = false; |  | ||||||
|         emitStatusChange(); |  | ||||||
|       } |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
| })(); | })(); | ||||||
|  |  | ||||||
|  | @ -1,27 +1,23 @@ | ||||||
| /* global | /* global | ||||||
|   API_METHODS |   API | ||||||
|   calcStyleDigest |   calcStyleDigest | ||||||
|   chromeLocal |   chromeLocal | ||||||
|   debounce |   debounce | ||||||
|   download |   download | ||||||
|   getStyleWithNoCode |  | ||||||
|   ignoreChromeError |   ignoreChromeError | ||||||
|   prefs |   prefs | ||||||
|   semverCompare |   semverCompare | ||||||
|   styleJSONseemsValid |   styleJSONseemsValid | ||||||
|   styleManager |  | ||||||
|   styleSectionsEqual |   styleSectionsEqual | ||||||
|   tryJSONparse |  | ||||||
|   usercss |   usercss | ||||||
| */ | */ | ||||||
| 'use strict'; | 'use strict'; | ||||||
| 
 | 
 | ||||||
| (() => { | (() => { | ||||||
| 
 |   const STATES = /** @namespace UpdaterStates */{ | ||||||
|   const STATES = { |  | ||||||
|     UPDATED: 'updated', |     UPDATED: 'updated', | ||||||
|     SKIPPED: 'skipped', |     SKIPPED: 'skipped', | ||||||
| 
 |     UNREACHABLE: 'server unreachable', | ||||||
|     // details for SKIPPED status
 |     // details for SKIPPED status
 | ||||||
|     EDITED:        'locally edited', |     EDITED:        'locally edited', | ||||||
|     MAYBE_EDITED:  'may be locally edited', |     MAYBE_EDITED:  'may be locally edited', | ||||||
|  | @ -32,20 +28,22 @@ | ||||||
|     ERROR_JSON:    'error: JSON is invalid', |     ERROR_JSON:    'error: JSON is invalid', | ||||||
|     ERROR_VERSION: 'error: version is older than installed style', |     ERROR_VERSION: 'error: version is older than installed style', | ||||||
|   }; |   }; | ||||||
| 
 |  | ||||||
|   const ALARM_NAME = 'scheduledUpdate'; |   const ALARM_NAME = 'scheduledUpdate'; | ||||||
|   const MIN_INTERVAL_MS = 60e3; |   const MIN_INTERVAL_MS = 60e3; | ||||||
| 
 |   const RETRY_ERRORS = [ | ||||||
|  |     503, // service unavailable
 | ||||||
|  |     429, // too many requests
 | ||||||
|  |   ]; | ||||||
|   let lastUpdateTime; |   let lastUpdateTime; | ||||||
|   let checkingAll = false; |   let checkingAll = false; | ||||||
|   let logQueue = []; |   let logQueue = []; | ||||||
|   let logLastWriteTime = 0; |   let logLastWriteTime = 0; | ||||||
| 
 | 
 | ||||||
|   const retrying = new Set(); |   API.updater = { | ||||||
| 
 |     checkAllStyles, | ||||||
|   API_METHODS.updateCheckAll = checkAllStyles; |     checkStyle, | ||||||
|   API_METHODS.updateCheck = checkStyle; |     getStates: () => STATES, | ||||||
|   API_METHODS.getUpdaterStates = () => STATES; |   }; | ||||||
| 
 | 
 | ||||||
|   chromeLocal.getValue('lastUpdateTime').then(val => { |   chromeLocal.getValue('lastUpdateTime').then(val => { | ||||||
|     lastUpdateTime = val || Date.now(); |     lastUpdateTime = val || Date.now(); | ||||||
|  | @ -53,191 +51,159 @@ | ||||||
|     chrome.alarms.onAlarm.addListener(onAlarm); |     chrome.alarms.onAlarm.addListener(onAlarm); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   return {checkAllStyles, checkStyle, STATES}; |   async function checkAllStyles({ | ||||||
| 
 |  | ||||||
|   function checkAllStyles({ |  | ||||||
|     save = true, |     save = true, | ||||||
|     ignoreDigest, |     ignoreDigest, | ||||||
|     observe, |     observe, | ||||||
|   } = {}) { |   } = {}) { | ||||||
|     resetInterval(); |     resetInterval(); | ||||||
|     checkingAll = true; |     checkingAll = true; | ||||||
|     retrying.clear(); |  | ||||||
|     const port = observe && chrome.runtime.connect({name: 'updater'}); |     const port = observe && chrome.runtime.connect({name: 'updater'}); | ||||||
|     return styleManager.getAllStyles().then(styles => { |     const styles = (await API.styles.getAll()) | ||||||
|       styles = styles.filter(style => style.updateUrl); |       .filter(style => style.updateUrl); | ||||||
|       if (port) port.postMessage({count: styles.length}); |     if (port) port.postMessage({count: styles.length}); | ||||||
|       log(''); |     log(''); | ||||||
|       log(`${save ? 'Scheduled' : 'Manual'} update check for ${styles.length} styles`); |     log(`${save ? 'Scheduled' : 'Manual'} update check for ${styles.length} styles`); | ||||||
|       return Promise.all( |     await Promise.all( | ||||||
|         styles.map(style => |       styles.map(style => | ||||||
|           checkStyle({style, port, save, ignoreDigest}))); |         checkStyle({style, port, save, ignoreDigest}))); | ||||||
|     }).then(() => { |     if (port) port.postMessage({done: true}); | ||||||
|       if (port) port.postMessage({done: true}); |     if (port) port.disconnect(); | ||||||
|       if (port) port.disconnect(); |     log(''); | ||||||
|       log(''); |     checkingAll = false; | ||||||
|       checkingAll = false; |  | ||||||
|       retrying.clear(); |  | ||||||
|     }); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   function checkStyle({ |   /** | ||||||
|     id, |    * @param {{ | ||||||
|     style, |       id?: number | ||||||
|     port, |       style?: StyleObj | ||||||
|     save = true, |       port?: chrome.runtime.Port | ||||||
|     ignoreDigest, |       save?: boolean = true | ||||||
|   }) { |       ignoreDigest?: boolean | ||||||
|     /* |     }} opts | ||||||
|     Original style digests are calculated in these cases: |    * @returns {{ | ||||||
|     * style is installed or updated from server |       style: StyleObj | ||||||
|     * style is checked for an update and its code is equal to the server code |       updated?: boolean | ||||||
|  |       error?: any | ||||||
|  |       STATES: UpdaterStates | ||||||
|  |      }} | ||||||
| 
 | 
 | ||||||
|     Update check proceeds in these cases: |    Original style digests are calculated in these cases: | ||||||
|     * style has the original digest and it's equal to the current digest |    * style is installed or updated from server | ||||||
|     * [ignoreDigest: true] style doesn't yet have the original digest but we ignore it |    * non-usercss style is checked for an update and styleSectionsEqual considers it unchanged | ||||||
|     * [ignoreDigest: none/false] style doesn't yet have the original digest |  | ||||||
|       so we compare the code to the server code and if it's the same we save the digest, |  | ||||||
|       otherwise we skip the style and report MAYBE_EDITED status |  | ||||||
| 
 | 
 | ||||||
|     'ignoreDigest' option is set on the second manual individual update check on the manage page. |    Update check proceeds in these cases: | ||||||
|     */ |    * style has the original digest and it's equal to the current digest | ||||||
|     return fetchStyle() |    * [ignoreDigest: true] style doesn't yet have the original digest but we ignore it | ||||||
|       .then(() => { |    * [ignoreDigest: none/false] style doesn't yet have the original digest | ||||||
|         if (!ignoreDigest) { |    so we compare the code to the server code and if it's the same we save the digest, | ||||||
|           return calcStyleDigest(style) |    otherwise we skip the style and report MAYBE_EDITED status | ||||||
|             .then(checkIfEdited); |  | ||||||
|         } |  | ||||||
|       }) |  | ||||||
|       .then(() => { |  | ||||||
|         if (style.usercssData) { |  | ||||||
|           return maybeUpdateUsercss(); |  | ||||||
|         } |  | ||||||
|         return maybeUpdateUSO(); |  | ||||||
|       }) |  | ||||||
|       .then(maybeSave) |  | ||||||
|       .then(reportSuccess) |  | ||||||
|       .catch(reportFailure); |  | ||||||
| 
 | 
 | ||||||
|     function fetchStyle() { |    'ignoreDigest' option is set on the second manual individual update check on the manage page. | ||||||
|       if (style) { |    */ | ||||||
|         return Promise.resolve(); |   async function checkStyle(opts) { | ||||||
|       } |     const { | ||||||
|       return styleManager.get(id) |       id, | ||||||
|         .then(style_ => { |       style = await API.styles.get(id), | ||||||
|           style = style_; |       ignoreDigest, | ||||||
|         }); |       port, | ||||||
|  |       save, | ||||||
|  |     } = opts; | ||||||
|  |     const ucd = style.usercssData; | ||||||
|  |     let res, state; | ||||||
|  |     try { | ||||||
|  |       await checkIfEdited(); | ||||||
|  |       res = { | ||||||
|  |         style: await (ucd ? updateUsercss : updateUSO)().then(maybeSave), | ||||||
|  |         updated: true, | ||||||
|  |       }; | ||||||
|  |       state = STATES.UPDATED; | ||||||
|  |     } catch (err) { | ||||||
|  |       const error = err === 0 && STATES.UNREACHABLE || | ||||||
|  |         err && err.message || | ||||||
|  |         err; | ||||||
|  |       res = {error, style, STATES}; | ||||||
|  |       state = `${STATES.SKIPPED} (${error})`; | ||||||
|     } |     } | ||||||
|  |     log(`${state} #${style.id} ${style.customName || style.name}`); | ||||||
|  |     if (port) port.postMessage(res); | ||||||
|  |     return res; | ||||||
| 
 | 
 | ||||||
|     function reportSuccess(saved) { |     async function checkIfEdited() { | ||||||
|       log(STATES.UPDATED + ` #${style.id} ${style.customName || style.name}`); |       if (!ignoreDigest && | ||||||
|       const info = {updated: true, style: saved}; |           style.originalDigest && | ||||||
|       if (port) port.postMessage(info); |           style.originalDigest !== await calcStyleDigest(style)) { | ||||||
|       return info; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     function reportFailure(error) { |  | ||||||
|       if (( |  | ||||||
|         error === 503 || // Service Unavailable
 |  | ||||||
|         error === 429    // Too Many Requests
 |  | ||||||
|       ) && !retrying.has(id)) { |  | ||||||
|         retrying.add(id); |  | ||||||
|         return new Promise(resolve => { |  | ||||||
|           setTimeout(() => { |  | ||||||
|             resolve(checkStyle({id, style, port, save, ignoreDigest})); |  | ||||||
|           }, 1000); |  | ||||||
|         }); |  | ||||||
|       } |  | ||||||
|       error = error === 0 ? 'server unreachable' : error; |  | ||||||
|       // UserCSS metadata error returns an object; e.g. "Invalid @var color..."
 |  | ||||||
|       if (typeof error === 'object' && error.message) { |  | ||||||
|         error = error.message; |  | ||||||
|       } |  | ||||||
|       log(STATES.SKIPPED + ` (${error}) #${style.id} ${style.customName || style.name}`); |  | ||||||
|       const info = {error, STATES, style: getStyleWithNoCode(style)}; |  | ||||||
|       if (port) port.postMessage(info); |  | ||||||
|       return info; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     function checkIfEdited(digest) { |  | ||||||
|       if (style.originalDigest && style.originalDigest !== digest) { |  | ||||||
|         return Promise.reject(STATES.EDITED); |         return Promise.reject(STATES.EDITED); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     function maybeUpdateUSO() { |     async function updateUSO() { | ||||||
|       return download(style.md5Url).then(md5 => { |       const md5 = await tryDownload(style.md5Url); | ||||||
|         if (!md5 || md5.length !== 32) { |       if (!md5 || md5.length !== 32) { | ||||||
|           return Promise.reject(STATES.ERROR_MD5); |         return Promise.reject(STATES.ERROR_MD5); | ||||||
|         } |       } | ||||||
|         if (md5 === style.originalMd5 && style.originalDigest && !ignoreDigest) { |       if (md5 === style.originalMd5 && style.originalDigest && !ignoreDigest) { | ||||||
|           return Promise.reject(STATES.SAME_MD5); |         return Promise.reject(STATES.SAME_MD5); | ||||||
|         } |       } | ||||||
|         // USO can't handle POST requests for style json
 |       const json = await tryDownload(style.updateUrl, {responseType: 'json'}); | ||||||
|         return download(style.updateUrl, {body: null}) |       if (!styleJSONseemsValid(json)) { | ||||||
|           .then(text => { |  | ||||||
|             const style = tryJSONparse(text); |  | ||||||
|             if (style) { |  | ||||||
|               // USO may not provide a correctly updated originalMd5 (#555)
 |  | ||||||
|               style.originalMd5 = md5; |  | ||||||
|             } |  | ||||||
|             return style; |  | ||||||
|           }); |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     function maybeUpdateUsercss() { |  | ||||||
|       // TODO: when sourceCode is > 100kB use http range request(s) for version check
 |  | ||||||
|       return download(style.updateUrl).then(text => |  | ||||||
|         usercss.buildMeta(text).then(json => { |  | ||||||
|           const {usercssData: {version}} = style; |  | ||||||
|           const {usercssData: {version: newVersion}} = json; |  | ||||||
|           switch (Math.sign(semverCompare(version, newVersion))) { |  | ||||||
|             case 0: |  | ||||||
|               // re-install is invalid in a soft upgrade
 |  | ||||||
|               if (!ignoreDigest) { |  | ||||||
|                 const sameCode = text === style.sourceCode; |  | ||||||
|                 return Promise.reject(sameCode ? STATES.SAME_CODE : STATES.SAME_VERSION); |  | ||||||
|               } |  | ||||||
|               break; |  | ||||||
|             case 1: |  | ||||||
|               // downgrade is always invalid
 |  | ||||||
|               return Promise.reject(STATES.ERROR_VERSION); |  | ||||||
|           } |  | ||||||
|           return usercss.buildCode(json); |  | ||||||
|         }) |  | ||||||
|       ); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     function maybeSave(json = {}) { |  | ||||||
|       // usercss is already validated while building
 |  | ||||||
|       if (!json.usercssData && !styleJSONseemsValid(json)) { |  | ||||||
|         return Promise.reject(STATES.ERROR_JSON); |         return Promise.reject(STATES.ERROR_JSON); | ||||||
|       } |       } | ||||||
|  |       // USO may not provide a correctly updated originalMd5 (#555)
 | ||||||
|  |       json.originalMd5 = md5; | ||||||
|  |       return json; | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|  |     async function updateUsercss() { | ||||||
|  |       // TODO: when sourceCode is > 100kB use http range request(s) for version check
 | ||||||
|  |       const text = await tryDownload(style.updateUrl); | ||||||
|  |       const json = await usercss.buildMeta(text); | ||||||
|  |       const delta = semverCompare(json.usercssData.version, ucd.version); | ||||||
|  |       if (!delta && !ignoreDigest) { | ||||||
|  |         // re-install is invalid in a soft upgrade
 | ||||||
|  |         const sameCode = text === style.sourceCode; | ||||||
|  |         return Promise.reject(sameCode ? STATES.SAME_CODE : STATES.SAME_VERSION); | ||||||
|  |       } | ||||||
|  |       if (delta < 0) { | ||||||
|  |         // downgrade is always invalid
 | ||||||
|  |         return Promise.reject(STATES.ERROR_VERSION); | ||||||
|  |       } | ||||||
|  |       return usercss.buildCode(json); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async function maybeSave(json) { | ||||||
|       json.id = style.id; |       json.id = style.id; | ||||||
|       json.updateDate = Date.now(); |       json.updateDate = Date.now(); | ||||||
| 
 |  | ||||||
|       // keep current state
 |       // keep current state
 | ||||||
|  |       delete json.customName; | ||||||
|       delete json.enabled; |       delete json.enabled; | ||||||
| 
 |  | ||||||
|       const newStyle = Object.assign({}, style, json); |       const newStyle = Object.assign({}, style, json); | ||||||
|       if (!style.usercssData && styleSectionsEqual(json, style)) { |       // update digest even if save === false as there might be just a space added etc.
 | ||||||
|         // update digest even if save === false as there might be just a space added etc.
 |       if (!ucd && styleSectionsEqual(json, style)) { | ||||||
|         return styleManager.installStyle(newStyle) |         style.originalDigest = (await API.styles.install(newStyle)).originalDigest; | ||||||
|           .then(saved => { |         return Promise.reject(STATES.SAME_CODE); | ||||||
|             style.originalDigest = saved.originalDigest; |  | ||||||
|             return Promise.reject(STATES.SAME_CODE); |  | ||||||
|           }); |  | ||||||
|       } |       } | ||||||
| 
 |  | ||||||
|       if (!style.originalDigest && !ignoreDigest) { |       if (!style.originalDigest && !ignoreDigest) { | ||||||
|         return Promise.reject(STATES.MAYBE_EDITED); |         return Promise.reject(STATES.MAYBE_EDITED); | ||||||
|       } |       } | ||||||
|  |       return !save ? newStyle : | ||||||
|  |         (ucd ? API.usercss : API.styles).install(newStyle); | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|       return save ? |     async function tryDownload(url, params) { | ||||||
|         API_METHODS[json.usercssData ? 'installUsercss' : 'installStyle'](newStyle) : |       let {retryDelay = 1000} = opts; | ||||||
|         newStyle; |       while (true) { | ||||||
|  |         try { | ||||||
|  |           return await download(url, params); | ||||||
|  |         } catch (code) { | ||||||
|  |           if (!RETRY_ERRORS.includes(code) || | ||||||
|  |               retryDelay > MIN_INTERVAL_MS) { | ||||||
|  |             return Promise.reject(code); | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |         retryDelay *= 1.25; | ||||||
|  |         await new Promise(resolve => setTimeout(resolve, retryDelay)); | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										81
									
								
								background/usercss-api-helper.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								background/usercss-api-helper.js
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,81 @@ | ||||||
|  | /* global | ||||||
|  |   API | ||||||
|  |   deepCopy | ||||||
|  |   usercss | ||||||
|  | */ | ||||||
|  | 'use strict'; | ||||||
|  | 
 | ||||||
|  | API.usercss = { | ||||||
|  | 
 | ||||||
|  |   async build({ | ||||||
|  |     styleId, | ||||||
|  |     sourceCode, | ||||||
|  |     vars, | ||||||
|  |     checkDup, | ||||||
|  |     metaOnly, | ||||||
|  |     assignVars, | ||||||
|  |   }) { | ||||||
|  |     let style = await usercss.buildMeta(sourceCode); | ||||||
|  |     const dup = (checkDup || assignVars) && | ||||||
|  |       await API.usercss.find(styleId ? {id: styleId} : style); | ||||||
|  |     if (!metaOnly) { | ||||||
|  |       if (vars || assignVars) { | ||||||
|  |         await usercss.assignVars(style, vars ? {usercssData: {vars}} : dup); | ||||||
|  |       } | ||||||
|  |       style = await usercss.buildCode(style); | ||||||
|  |     } | ||||||
|  |     return {style, dup}; | ||||||
|  |   }, | ||||||
|  | 
 | ||||||
|  |   async buildMeta(style) { | ||||||
|  |     if (style.usercssData) { | ||||||
|  |       return style; | ||||||
|  |     } | ||||||
|  |     // allow sourceCode to be normalized
 | ||||||
|  |     const {sourceCode} = style; | ||||||
|  |     delete style.sourceCode; | ||||||
|  |     return Object.assign(await usercss.buildMeta(sourceCode), style); | ||||||
|  |   }, | ||||||
|  | 
 | ||||||
|  |   async configVars(id, vars) { | ||||||
|  |     let style = deepCopy(await API.styles.get(id)); | ||||||
|  |     style.usercssData.vars = vars; | ||||||
|  |     style = await usercss.buildCode(style); | ||||||
|  |     style = await API.styles.install(style, 'config'); | ||||||
|  |     return style.usercssData.vars; | ||||||
|  |   }, | ||||||
|  | 
 | ||||||
|  |   async editSave(style) { | ||||||
|  |     return API.styles.editSave(await API.usercss.parse(style)); | ||||||
|  |   }, | ||||||
|  | 
 | ||||||
|  |   async find(styleOrData) { | ||||||
|  |     if (styleOrData.id) { | ||||||
|  |       return API.styles.get(styleOrData.id); | ||||||
|  |     } | ||||||
|  |     const {name, namespace} = styleOrData.usercssData || styleOrData; | ||||||
|  |     for (const dup of await API.styles.getAll()) { | ||||||
|  |       const data = dup.usercssData; | ||||||
|  |       if (data && | ||||||
|  |         data.name === name && | ||||||
|  |         data.namespace === namespace) { | ||||||
|  |         return dup; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  | 
 | ||||||
|  |   async install(style) { | ||||||
|  |     return API.styles.install(await API.usercss.parse(style)); | ||||||
|  |   }, | ||||||
|  | 
 | ||||||
|  |   async parse(style) { | ||||||
|  |     style = await API.usercss.buildMeta(style); | ||||||
|  |     // preserve style.vars during update
 | ||||||
|  |     const dup = await API.usercss.find(style); | ||||||
|  |     if (dup) { | ||||||
|  |       style.id = dup.id; | ||||||
|  |       await usercss.assignVars(style, dup); | ||||||
|  |     } | ||||||
|  |     return usercss.buildCode(style); | ||||||
|  |   }, | ||||||
|  | }; | ||||||
|  | @ -1,132 +0,0 @@ | ||||||
| /* global API_METHODS usercss styleManager deepCopy */ |  | ||||||
| /* exported usercssHelper */ |  | ||||||
| 'use strict'; |  | ||||||
| 
 |  | ||||||
| const usercssHelper = (() => { |  | ||||||
|   API_METHODS.installUsercss = installUsercss; |  | ||||||
|   API_METHODS.editSaveUsercss = editSaveUsercss; |  | ||||||
|   API_METHODS.configUsercssVars = configUsercssVars; |  | ||||||
| 
 |  | ||||||
|   API_METHODS.buildUsercss = build; |  | ||||||
|   API_METHODS.buildUsercssMeta = buildMeta; |  | ||||||
|   API_METHODS.findUsercss = find; |  | ||||||
| 
 |  | ||||||
|   function buildMeta(style) { |  | ||||||
|     if (style.usercssData) { |  | ||||||
|       return Promise.resolve(style); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // allow sourceCode to be normalized
 |  | ||||||
|     const {sourceCode} = style; |  | ||||||
|     delete style.sourceCode; |  | ||||||
| 
 |  | ||||||
|     return usercss.buildMeta(sourceCode) |  | ||||||
|       .then(newStyle => Object.assign(newStyle, style)); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function assignVars(style) { |  | ||||||
|     return find(style) |  | ||||||
|       .then(dup => { |  | ||||||
|         if (dup) { |  | ||||||
|           style.id = dup.id; |  | ||||||
|           // preserve style.vars during update
 |  | ||||||
|           return usercss.assignVars(style, dup) |  | ||||||
|             .then(() => style); |  | ||||||
|         } |  | ||||||
|         return style; |  | ||||||
|       }); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /** |  | ||||||
|    * Parse the source, find the duplication, and build sections with variables |  | ||||||
|    * @param _ |  | ||||||
|    * @param {String} _.sourceCode |  | ||||||
|    * @param {Boolean=} _.checkDup |  | ||||||
|    * @param {Boolean=} _.metaOnly |  | ||||||
|    * @param {Object} _.vars |  | ||||||
|    * @param {Boolean=} _.assignVars |  | ||||||
|    * @returns {Promise<{style, dup:Boolean?}>} |  | ||||||
|    */ |  | ||||||
|   function build({ |  | ||||||
|     styleId, |  | ||||||
|     sourceCode, |  | ||||||
|     checkDup, |  | ||||||
|     metaOnly, |  | ||||||
|     vars, |  | ||||||
|     assignVars = false, |  | ||||||
|   }) { |  | ||||||
|     return usercss.buildMeta(sourceCode) |  | ||||||
|       .then(style => { |  | ||||||
|         const findDup = checkDup || assignVars ? |  | ||||||
|           find(styleId ? {id: styleId} : style) : Promise.resolve(); |  | ||||||
|         return Promise.all([ |  | ||||||
|           metaOnly ? style : doBuild(style, findDup), |  | ||||||
|           findDup, |  | ||||||
|         ]); |  | ||||||
|       }) |  | ||||||
|       .then(([style, dup]) => ({style, dup})); |  | ||||||
| 
 |  | ||||||
|     function doBuild(style, findDup) { |  | ||||||
|       if (vars || assignVars) { |  | ||||||
|         const getOld = vars ? Promise.resolve({usercssData: {vars}}) : findDup; |  | ||||||
|         return getOld |  | ||||||
|           .then(oldStyle => usercss.assignVars(style, oldStyle)) |  | ||||||
|           .then(() => usercss.buildCode(style)); |  | ||||||
|       } |  | ||||||
|       return usercss.buildCode(style); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   // Build the style within aditional properties then inherit variable values
 |  | ||||||
|   // from the old style.
 |  | ||||||
|   function parse(style) { |  | ||||||
|     return buildMeta(style) |  | ||||||
|       .then(buildMeta) |  | ||||||
|       .then(assignVars) |  | ||||||
|       .then(usercss.buildCode); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   // FIXME: simplify this to `installUsercss(sourceCode)`?
 |  | ||||||
|   function installUsercss(style) { |  | ||||||
|     return parse(style) |  | ||||||
|       .then(styleManager.installStyle); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   // FIXME: simplify this to `editSaveUsercss({sourceCode, exclusions})`?
 |  | ||||||
|   function editSaveUsercss(style) { |  | ||||||
|     return parse(style) |  | ||||||
|       .then(styleManager.editSave); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function configUsercssVars(id, vars) { |  | ||||||
|     return styleManager.get(id) |  | ||||||
|       .then(style => { |  | ||||||
|         const newStyle = deepCopy(style); |  | ||||||
|         newStyle.usercssData.vars = vars; |  | ||||||
|         return usercss.buildCode(newStyle); |  | ||||||
|       }) |  | ||||||
|       .then(style => styleManager.installStyle(style, 'config')) |  | ||||||
|       .then(style => style.usercssData.vars); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /** |  | ||||||
|    * @param {Style|{name:string, namespace:string}} styleOrData |  | ||||||
|    * @returns {Style} |  | ||||||
|    */ |  | ||||||
|   function find(styleOrData) { |  | ||||||
|     if (styleOrData.id) { |  | ||||||
|       return styleManager.get(styleOrData.id); |  | ||||||
|     } |  | ||||||
|     const {name, namespace} = styleOrData.usercssData || styleOrData; |  | ||||||
|     return styleManager.getAllStyles().then(styleList => { |  | ||||||
|       for (const dup of styleList) { |  | ||||||
|         const data = dup.usercssData; |  | ||||||
|         if (!data) continue; |  | ||||||
|         if (data.name === name && |  | ||||||
|             data.namespace === namespace) { |  | ||||||
|           return dup; |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| })(); |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| /* global | /* global | ||||||
|   API_METHODS |   API | ||||||
|   download |   download | ||||||
|   openURL |   openURL | ||||||
|   tabManager |   tabManager | ||||||
|  | @ -25,7 +25,7 @@ | ||||||
|       isContentTypeText((await fetch(url, {method: 'HEAD'})).headers.get('content-type')) |       isContentTypeText((await fetch(url, {method: 'HEAD'})).headers.get('content-type')) | ||||||
|     ) && download(url); |     ) && download(url); | ||||||
| 
 | 
 | ||||||
|   API_METHODS.getUsercssInstallCode = url => { |   API.usercss.getInstallCode = url => { | ||||||
|     // when the installer tab is reloaded after the cache is expired, this will throw intentionally
 |     // when the installer tab is reloaded after the cache is expired, this will throw intentionally
 | ||||||
|     const {code, timer} = installCodeCache[url]; |     const {code, timer} = installCodeCache[url]; | ||||||
|     clearInstallCode(url); |     clearInstallCode(url); | ||||||
|  |  | ||||||
|  | @ -60,7 +60,7 @@ self.INJECTED !== 1 && (() => { | ||||||
|       await API.styleViaAPI({method: 'styleApply'}); |       await API.styleViaAPI({method: 'styleApply'}); | ||||||
|     } else { |     } else { | ||||||
|       const styles = chrome.app && !chrome.tabs && getStylesViaXhr() || |       const styles = chrome.app && !chrome.tabs && getStylesViaXhr() || | ||||||
|         await API.getSectionsByUrl(getMatchUrl(), null, true); |         await API.styles.getSectionsByUrl(getMatchUrl(), null, true); | ||||||
|       if (styles.disableAll) { |       if (styles.disableAll) { | ||||||
|         delete styles.disableAll; |         delete styles.disableAll; | ||||||
|         styleInjector.toggle(false); |         styleInjector.toggle(false); | ||||||
|  | @ -117,7 +117,7 @@ self.INJECTED !== 1 && (() => { | ||||||
| 
 | 
 | ||||||
|       case 'styleUpdated': |       case 'styleUpdated': | ||||||
|         if (request.style.enabled) { |         if (request.style.enabled) { | ||||||
|           API.getSectionsByUrl(getMatchUrl(), request.style.id) |           API.styles.getSectionsByUrl(getMatchUrl(), request.style.id) | ||||||
|             .then(sections => { |             .then(sections => { | ||||||
|               if (!sections[request.style.id]) { |               if (!sections[request.style.id]) { | ||||||
|                 styleInjector.remove(request.style.id); |                 styleInjector.remove(request.style.id); | ||||||
|  | @ -132,13 +132,13 @@ self.INJECTED !== 1 && (() => { | ||||||
| 
 | 
 | ||||||
|       case 'styleAdded': |       case 'styleAdded': | ||||||
|         if (request.style.enabled) { |         if (request.style.enabled) { | ||||||
|           API.getSectionsByUrl(getMatchUrl(), request.style.id) |           API.styles.getSectionsByUrl(getMatchUrl(), request.style.id) | ||||||
|             .then(styleInjector.apply); |             .then(styleInjector.apply); | ||||||
|         } |         } | ||||||
|         break; |         break; | ||||||
| 
 | 
 | ||||||
|       case 'urlChanged': |       case 'urlChanged': | ||||||
|         API.getSectionsByUrl(getMatchUrl()) |         API.styles.getSectionsByUrl(getMatchUrl()) | ||||||
|           .then(styleInjector.replace); |           .then(styleInjector.replace); | ||||||
|         break; |         break; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -13,7 +13,7 @@ if (window.INJECTED_GREASYFORK !== 1) { | ||||||
|         e.data.name && |         e.data.name && | ||||||
|         e.data.type === 'style-version-query') { |         e.data.type === 'style-version-query') { | ||||||
|       removeEventListener('message', onMessage); |       removeEventListener('message', onMessage); | ||||||
|       const style = await API.findUsercss(e.data) || {}; |       const style = await API.usercss.find(e.data) || {}; | ||||||
|       const {version} = style.usercssData || {}; |       const {version} = style.usercssData || {}; | ||||||
|       postMessage({type: 'style-version', version}, '*'); |       postMessage({type: 'style-version', version}, '*'); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -34,7 +34,7 @@ | ||||||
|     && event.data.type === 'ouc-is-installed' |     && event.data.type === 'ouc-is-installed' | ||||||
|     && allowedOrigins.includes(event.origin) |     && allowedOrigins.includes(event.origin) | ||||||
|     ) { |     ) { | ||||||
|       API.findUsercss({ |       API.usercss.find({ | ||||||
|         name: event.data.name, |         name: event.data.name, | ||||||
|         namespace: event.data.namespace, |         namespace: event.data.namespace, | ||||||
|       }).then(style => { |       }).then(style => { | ||||||
|  | @ -129,7 +129,7 @@ | ||||||
|     && event.data.type === 'ouc-install-usercss' |     && event.data.type === 'ouc-install-usercss' | ||||||
|     && allowedOrigins.includes(event.origin) |     && allowedOrigins.includes(event.origin) | ||||||
|     ) { |     ) { | ||||||
|       API.installUsercss({ |       API.usercss.install({ | ||||||
|         name: event.data.title, |         name: event.data.title, | ||||||
|         sourceCode: event.data.code, |         sourceCode: event.data.code, | ||||||
|       }).then(style => { |       }).then(style => { | ||||||
|  |  | ||||||
|  | @ -1,19 +1,21 @@ | ||||||
| 'use strict'; | 'use strict'; | ||||||
| 
 | 
 | ||||||
| // preventing reregistration if reinjected by tabs.executeScript for whatever reason, just in case
 | // preventing reregistration if reinjected by tabs.executeScript for whatever reason, just in case
 | ||||||
| if (typeof self.oldCode !== 'string') { | if (typeof window.oldCode !== 'string') { | ||||||
|   self.oldCode = (document.querySelector('body > pre') || document.body).textContent; |   window.oldCode = (document.querySelector('body > pre') || document.body).textContent; | ||||||
|   chrome.runtime.onConnect.addListener(port => { |   chrome.runtime.onConnect.addListener(port => { | ||||||
|     if (port.name !== 'downloadSelf') return; |     if (port.name !== 'downloadSelf') return; | ||||||
|     port.onMessage.addListener(({id, force}) => { |     port.onMessage.addListener(async ({id, force}) => { | ||||||
|       fetch(location.href, {mode: 'same-origin'}) |       const msg = {id}; | ||||||
|         .then(r => r.text()) |       try { | ||||||
|         .then(code => ({id, code: force || code !== self.oldCode ? code : null})) |         const code = await (await fetch(location.href, {mode: 'same-origin'})).text(); | ||||||
|         .catch(error => ({id, error: error.message || `${error}`})) |         if (code !== window.oldCode || force) { | ||||||
|         .then(msg => { |           msg.code = window.oldCode = code; | ||||||
|           port.postMessage(msg); |         } | ||||||
|           if (msg.code != null) self.oldCode = msg.code; |       } catch (error) { | ||||||
|         }); |         msg.error = error.message || `${error}`; | ||||||
|  |       } | ||||||
|  |       port.postMessage(msg); | ||||||
|     }); |     }); | ||||||
|     // FF keeps content scripts connected on navigation https://github.com/openstyles/stylus/issues/864
 |     // FF keeps content scripts connected on navigation https://github.com/openstyles/stylus/issues/864
 | ||||||
|     addEventListener('pagehide', () => port.disconnect(), {once: true}); |     addEventListener('pagehide', () => port.disconnect(), {once: true}); | ||||||
|  | @ -21,4 +23,4 @@ if (typeof self.oldCode !== 'string') { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // passing the result to tabs.executeScript
 | // passing the result to tabs.executeScript
 | ||||||
| self.oldCode; // eslint-disable-line no-unused-expressions
 | window.oldCode; // eslint-disable-line no-unused-expressions
 | ||||||
|  |  | ||||||
|  | @ -24,7 +24,7 @@ | ||||||
|   let currentMd5; |   let currentMd5; | ||||||
|   const md5Url = getMeta('stylish-md5-url') || `https://update.userstyles.org/${styleId}.md5`; |   const md5Url = getMeta('stylish-md5-url') || `https://update.userstyles.org/${styleId}.md5`; | ||||||
|   Promise.all([ |   Promise.all([ | ||||||
|     API.findStyle({md5Url}), |     API.styles.find({md5Url}), | ||||||
|     getResource(md5Url), |     getResource(md5Url), | ||||||
|     onDOMready(), |     onDOMready(), | ||||||
|   ]).then(checkUpdatability); |   ]).then(checkUpdatability); | ||||||
|  | @ -154,9 +154,9 @@ | ||||||
| 
 | 
 | ||||||
|   function doInstall() { |   function doInstall() { | ||||||
|     let oldStyle; |     let oldStyle; | ||||||
|     return API.findStyle({ |     return API.styles.find({ | ||||||
|       md5Url: getMeta('stylish-md5-url') || location.href, |       md5Url: getMeta('stylish-md5-url') || location.href, | ||||||
|     }, true) |     }) | ||||||
|       .then(_oldStyle => { |       .then(_oldStyle => { | ||||||
|         oldStyle = _oldStyle; |         oldStyle = _oldStyle; | ||||||
|         return oldStyle ? |         return oldStyle ? | ||||||
|  | @ -187,7 +187,7 @@ | ||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
|       // Update originalMd5 since USO changed it (2018-11-11) to NOT match the current md5
 |       // Update originalMd5 since USO changed it (2018-11-11) to NOT match the current md5
 | ||||||
|       return API.installStyle(Object.assign(json, addProps, {originalMd5: currentMd5})) |       return API.styles.install(Object.assign(json, addProps, {originalMd5: currentMd5})) | ||||||
|         .then(style => { |         .then(style => { | ||||||
|           if (!isNew && style.updateUrl.includes('?')) { |           if (!isNew && style.updateUrl.includes('?')) { | ||||||
|             enableUpdateButton(true); |             enableUpdateButton(true); | ||||||
|  | @ -218,20 +218,15 @@ | ||||||
|     return e ? e.getAttribute('href') : null; |     return e ? e.getAttribute('href') : null; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   function getResource(url, options) { |   async function getResource(url, type = 'text') { | ||||||
|     if (url.startsWith('#')) { |     try { | ||||||
|       return Promise.resolve(document.getElementById(url.slice(1)).textContent); |       return url.startsWith('#') | ||||||
|  |         ? document.getElementById(url.slice(1)).textContent | ||||||
|  |         : await (await fetch(url))[type]; | ||||||
|  |     } catch (error) { | ||||||
|  |       alert('Error\n' + error.message); | ||||||
|  |       return Promise.reject(error); | ||||||
|     } |     } | ||||||
|     return API.download(Object.assign({ |  | ||||||
|       url, |  | ||||||
|       timeout: 60e3, |  | ||||||
|       // USO can't handle POST requests for style json
 |  | ||||||
|       body: null, |  | ||||||
|     }, options)) |  | ||||||
|       .catch(error => { |  | ||||||
|         alert('Error' + (error ? '\n' + error : '')); |  | ||||||
|         throw error; |  | ||||||
|       }); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   // USO providing md5Url as "https://update.update.userstyles.org/#####.md5"
 |   // USO providing md5Url as "https://update.update.userstyles.org/#####.md5"
 | ||||||
|  | @ -244,7 +239,7 @@ | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   function getStyleJson() { |   function getStyleJson() { | ||||||
|     return getResource(getStyleURL(), {responseType: 'json'}) |     return getResource(getStyleURL(), 'json') | ||||||
|       .then(style => { |       .then(style => { | ||||||
|         if (!style || !Array.isArray(style.sections) || style.sections.length) { |         if (!style || !Array.isArray(style.sections) || style.sections.length) { | ||||||
|           return style; |           return style; | ||||||
|  | @ -254,7 +249,7 @@ | ||||||
|           return style; |           return style; | ||||||
|         } |         } | ||||||
|         return getResource(getMeta('stylish-update-url')) |         return getResource(getMeta('stylish-update-url')) | ||||||
|           .then(code => API.parseCss({code})) |           .then(code => API.worker.parseMozFormat({code})) | ||||||
|           .then(result => { |           .then(result => { | ||||||
|             style.sections = result.sections; |             style.sections = result.sections; | ||||||
|             return style; |             return style; | ||||||
|  |  | ||||||
							
								
								
									
										22
									
								
								edit/edit.js
									
									
									
									
									
								
							
							
						
						
									
										22
									
								
								edit/edit.js
									
									
									
									
									
								
							|  | @ -110,7 +110,7 @@ lazyInit(); | ||||||
|   async function initStyle() { |   async function initStyle() { | ||||||
|     const params = new URLSearchParams(location.search); |     const params = new URLSearchParams(location.search); | ||||||
|     const id = Number(params.get('id')); |     const id = Number(params.get('id')); | ||||||
|     style = id ? await API.getStyle(id) : initEmptyStyle(params); |     style = id ? await API.styles.get(id) : initEmptyStyle(params); | ||||||
|     // switching the mode here to show the correct page ASAP, usually before DOMContentLoaded
 |     // switching the mode here to show the correct page ASAP, usually before DOMContentLoaded
 | ||||||
|     editor.isUsercss = Boolean(style.usercssData || !style.id && prefs.get('newStyleAsUsercss')); |     editor.isUsercss = Boolean(style.usercssData || !style.id && prefs.get('newStyleAsUsercss')); | ||||||
|     document.documentElement.classList.toggle('usercss', editor.isUsercss); |     document.documentElement.classList.toggle('usercss', editor.isUsercss); | ||||||
|  | @ -426,26 +426,18 @@ function lazyInit() { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function onRuntimeMessage(request) { | function onRuntimeMessage(request) { | ||||||
|  |   const {style} = request; | ||||||
|   switch (request.method) { |   switch (request.method) { | ||||||
|     case 'styleUpdated': |     case 'styleUpdated': | ||||||
|       if ( |       if (editor.style.id === style.id && | ||||||
|         editor.style.id === request.style.id && |           !['editPreview', 'editPreviewEnd', 'editSave', 'config'].includes(request.reason)) { | ||||||
|         !['editPreview', 'editPreviewEnd', 'editSave', 'config'] |         Promise.resolve(request.codeIsUpdated === false ? style : API.styles.get(style.id)) | ||||||
|           .includes(request.reason) |           .then(newStyle => editor.replaceStyle(newStyle, request.codeIsUpdated)); | ||||||
|       ) { |  | ||||||
|         Promise.resolve( |  | ||||||
|           request.codeIsUpdated === false ? |  | ||||||
|             request.style : API.getStyle(request.style.id) |  | ||||||
|         ) |  | ||||||
|           .then(newStyle => { |  | ||||||
|             editor.replaceStyle(newStyle, request.codeIsUpdated); |  | ||||||
|           }); |  | ||||||
|       } |       } | ||||||
|       break; |       break; | ||||||
|     case 'styleDeleted': |     case 'styleDeleted': | ||||||
|       if (editor.style.id === request.style.id) { |       if (editor.style.id === style.id) { | ||||||
|         closeCurrentTab(); |         closeCurrentTab(); | ||||||
|         break; |  | ||||||
|       } |       } | ||||||
|       break; |       break; | ||||||
|     case 'editDeleteText': |     case 'editDeleteText': | ||||||
|  |  | ||||||
|  | @ -117,7 +117,7 @@ function SectionsEditor() { | ||||||
|       if (!validate(newStyle)) { |       if (!validate(newStyle)) { | ||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
|       newStyle = await API.editSave(newStyle); |       newStyle = await API.styles.editSave(newStyle); | ||||||
|       destroyRemovedSections(); |       destroyRemovedSections(); | ||||||
|       sessionStore.justEditedStyleId = newStyle.id; |       sessionStore.justEditedStyleId = newStyle.id; | ||||||
|       editor.replaceStyle(newStyle, false); |       editor.replaceStyle(newStyle, false); | ||||||
|  | @ -384,7 +384,7 @@ function SectionsEditor() { | ||||||
|               t('importPreprocessor'), 'pre-line', |               t('importPreprocessor'), 'pre-line', | ||||||
|               t('importPreprocessorTitle')) |               t('importPreprocessorTitle')) | ||||||
|         ) { |         ) { | ||||||
|           const {sections, errors} = await API.parseCss({code}); |           const {sections, errors} = await API.worker.parseMozFormat({code}); | ||||||
|           // shouldn't happen but just in case
 |           // shouldn't happen but just in case
 | ||||||
|           if (!sections.length || errors.length) { |           if (!sections.length || errors.length) { | ||||||
|             throw errors; |             throw errors; | ||||||
|  | @ -403,7 +403,7 @@ function SectionsEditor() { | ||||||
| 
 | 
 | ||||||
|     async function getPreprocessor(code) { |     async function getPreprocessor(code) { | ||||||
|       try { |       try { | ||||||
|         return (await API.buildUsercssMeta({sourceCode: code})).usercssData.preprocessor; |         return (await API.usercss.buildMeta({sourceCode: code})).usercssData.preprocessor; | ||||||
|       } catch (e) {} |       } catch (e) {} | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -97,7 +97,7 @@ function SourceEditor() { | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   function preprocess(style) { |   function preprocess(style) { | ||||||
|     return API.buildUsercss({ |     return API.usercss.build({ | ||||||
|       styleId: style.id, |       styleId: style.id, | ||||||
|       sourceCode: style.sourceCode, |       sourceCode: style.sourceCode, | ||||||
|       assignVars: true, |       assignVars: true, | ||||||
|  | @ -231,7 +231,7 @@ function SourceEditor() { | ||||||
|     if (!dirty.isDirty()) return; |     if (!dirty.isDirty()) return; | ||||||
|     const code = cm.getValue(); |     const code = cm.getValue(); | ||||||
|     return ensureUniqueStyle(code) |     return ensureUniqueStyle(code) | ||||||
|       .then(() => API.editSaveUsercss({ |       .then(() => API.usercss.editSave({ | ||||||
|         id: style.id, |         id: style.id, | ||||||
|         enabled: style.enabled, |         enabled: style.enabled, | ||||||
|         sourceCode: code, |         sourceCode: code, | ||||||
|  | @ -265,7 +265,7 @@ function SourceEditor() { | ||||||
| 
 | 
 | ||||||
|   function ensureUniqueStyle(code) { |   function ensureUniqueStyle(code) { | ||||||
|     return style.id ? Promise.resolve() : |     return style.id ? Promise.resolve() : | ||||||
|       API.buildUsercss({ |       API.usercss.build({ | ||||||
|         sourceCode: code, |         sourceCode: code, | ||||||
|         checkDup: true, |         checkDup: true, | ||||||
|         metaOnly: true, |         metaOnly: true, | ||||||
|  |  | ||||||
|  | @ -176,7 +176,7 @@ | ||||||
|   function initSourceCode(sourceCode) { |   function initSourceCode(sourceCode) { | ||||||
|     cm.setValue(sourceCode); |     cm.setValue(sourceCode); | ||||||
|     cm.refresh(); |     cm.refresh(); | ||||||
|     API.buildUsercss({sourceCode, checkDup: true}) |     API.usercss.build({sourceCode, checkDup: true}) | ||||||
|       .then(init) |       .then(init) | ||||||
|       .catch(err => { |       .catch(err => { | ||||||
|         $('#header').classList.add('meta-init-error'); |         $('#header').classList.add('meta-init-error'); | ||||||
|  | @ -248,7 +248,7 @@ | ||||||
|           data.version, |           data.version, | ||||||
|         ])) |         ])) | ||||||
|       ).then(ok => ok && |       ).then(ok => ok && | ||||||
|         API.installUsercss(style) |         API.usercss.install(style) | ||||||
|           .then(install) |           .then(install) | ||||||
|           .catch(err => messageBox.alert(t('styleInstallFailed', err), 'pre')) |           .catch(err => messageBox.alert(t('styleInstallFailed', err), 'pre')) | ||||||
|       ); |       ); | ||||||
|  | @ -317,7 +317,7 @@ | ||||||
|     let sequence = null; |     let sequence = null; | ||||||
|     if (tabId < 0) { |     if (tabId < 0) { | ||||||
|       getData = DirectDownloader(); |       getData = DirectDownloader(); | ||||||
|       sequence = API.getUsercssInstallCode(initialUrl) |       sequence = API.usercss.getInstallCode(initialUrl) | ||||||
|         .then(code => code || getData()) |         .then(code => code || getData()) | ||||||
|         .catch(getData); |         .catch(getData); | ||||||
|     } else { |     } else { | ||||||
|  | @ -372,19 +372,20 @@ | ||||||
|         cm.setValue(code); |         cm.setValue(code); | ||||||
|         cm.setCursor(cursor); |         cm.setCursor(cursor); | ||||||
|         cm.scrollTo(scrollInfo.left, scrollInfo.top); |         cm.scrollTo(scrollInfo.left, scrollInfo.top); | ||||||
|         return API.installUsercss({id, sourceCode: code}) |         return API.usercss.install({id, sourceCode: code}) | ||||||
|           .then(updateMeta) |           .then(updateMeta) | ||||||
|           .catch(showError); |           .catch(showError); | ||||||
|       }); |       }); | ||||||
|     } |     } | ||||||
|     function DirectDownloader() { |     function DirectDownloader() { | ||||||
|       let oldCode = null; |       let oldCode = null; | ||||||
|       const passChangedCode = code => { |       return async () => { | ||||||
|         const isSame = code === oldCode; |         const code = await download(initialUrl); | ||||||
|         oldCode = code; |         if (oldCode !== code) { | ||||||
|         return isSame ? null : code; |           oldCode = code; | ||||||
|  |           return code; | ||||||
|  |         } | ||||||
|       }; |       }; | ||||||
|       return () => download(initialUrl).then(passChangedCode); |  | ||||||
|     } |     } | ||||||
|     function PortDownloader() { |     function PortDownloader() { | ||||||
|       const resolvers = new Map(); |       const resolvers = new Map(); | ||||||
|  |  | ||||||
|  | @ -84,10 +84,13 @@ const URLS = { | ||||||
|     url && |     url && | ||||||
|     url.startsWith(URLS.usoArchiveRaw) && |     url.startsWith(URLS.usoArchiveRaw) && | ||||||
|     parseInt(url.match(/\/(\d+)\.user\.css|$/)[1]), |     parseInt(url.match(/\/(\d+)\.user\.css|$/)[1]), | ||||||
|  |   extractUsoArchiveInstallUrl: url => { | ||||||
|  |     const id = URLS.extractUsoArchiveId(url); | ||||||
|  |     return id ? `${URLS.usoArchive}?style=${id}` : ''; | ||||||
|  |   }, | ||||||
| 
 | 
 | ||||||
|   extractGreasyForkId: url => |   extractGreasyForkInstallUrl: url => | ||||||
|     /^https:\/\/(?:greasy|sleazy)fork\.org\/scripts\/(\d+)[^/]*\/code\/[^/]*\.user\.css$/.test(url) && |     /^(https:\/\/(?:greasy|sleazy)fork\.org\/scripts\/\d+)[^/]*\/code\/[^/]*\.user\.css$|$/.exec(url)[1], | ||||||
|     RegExp.$1, |  | ||||||
| 
 | 
 | ||||||
|   supported: url => ( |   supported: url => ( | ||||||
|     url.startsWith('http') || |     url.startsWith('http') || | ||||||
|  | @ -98,9 +101,7 @@ const URLS = { | ||||||
|   ), |   ), | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| if (chrome.extension.getBackgroundPage && chrome.extension.getBackgroundPage() === window) { | if (!chrome.extension.getBackgroundPage || chrome.extension.getBackgroundPage() !== window) { | ||||||
|   window.API_METHODS = {}; |  | ||||||
| } else { |  | ||||||
|   const cls = FIREFOX ? 'firefox' : OPERA ? 'opera' : VIVALDI ? 'vivaldi' : ''; |   const cls = FIREFOX ? 'firefox' : OPERA ? 'opera' : VIVALDI ? 'vivaldi' : ''; | ||||||
|   if (cls) document.documentElement.classList.add(cls); |   if (cls) document.documentElement.classList.add(cls); | ||||||
| } | } | ||||||
|  | @ -226,8 +227,9 @@ function activateTab(tab, {url, index, openerTabId} = {}) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| function stringAsRegExp(s, flags) { | function stringAsRegExp(s, flags, asString) { | ||||||
|   return new RegExp(s.replace(/[{}()[\]\\.+*?^$|]/g, '\\$&'), flags); |   s = s.replace(/[{}()[\]\\.+*?^$|]/g, '\\$&'); | ||||||
|  |   return asString ? s : new RegExp(s, flags); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -371,70 +373,49 @@ function download(url, { | ||||||
|   requiredStatusCode = 200, |   requiredStatusCode = 200, | ||||||
|   timeout = 60e3, // connection timeout, USO is that bad
 |   timeout = 60e3, // connection timeout, USO is that bad
 | ||||||
|   loadTimeout = 2 * 60e3, // data transfer timeout (counted from the first remote response)
 |   loadTimeout = 2 * 60e3, // data transfer timeout (counted from the first remote response)
 | ||||||
|   headers = { |   headers, | ||||||
|     'Content-type': 'application/x-www-form-urlencoded', |  | ||||||
|   }, |  | ||||||
| } = {}) { | } = {}) { | ||||||
|   const queryPos = url.indexOf('?'); |   /* USO can't handle POST requests for style json and XHR/fetch can't handle super long URL | ||||||
|   if (queryPos > 0 && body === undefined) { |    * so we need to collapse all long variables and expand them in the response */ | ||||||
|     method = 'POST'; |   const queryPos = url.startsWith(URLS.uso) ? url.indexOf('?') : -1; | ||||||
|     body = url.slice(queryPos); |   if (queryPos >= 0) { | ||||||
|     url = url.slice(0, queryPos); |     if (body === undefined) { | ||||||
|  |       method = 'POST'; | ||||||
|  |       body = url.slice(queryPos); | ||||||
|  |       url = url.slice(0, queryPos); | ||||||
|  |     } | ||||||
|  |     if (headers === undefined) { | ||||||
|  |       headers = { | ||||||
|  |         'Content-type': 'application/x-www-form-urlencoded', | ||||||
|  |       }; | ||||||
|  |     } | ||||||
|   } |   } | ||||||
|   // * USO can't handle POST requests for style json
 |  | ||||||
|   // * XHR/fetch can't handle long URL
 |  | ||||||
|   // So we need to collapse all long variables and expand them in the response
 |  | ||||||
|   const usoVars = []; |   const usoVars = []; | ||||||
| 
 |  | ||||||
|   return new Promise((resolve, reject) => { |   return new Promise((resolve, reject) => { | ||||||
|     let xhr; |     const xhr = new XMLHttpRequest(); | ||||||
|     const u = new URL(collapseUsoVars(url)); |     const u = new URL(collapseUsoVars(url)); | ||||||
|     const onTimeout = () => { |     const onTimeout = () => { | ||||||
|       if (xhr) xhr.abort(); |       xhr.abort(); | ||||||
|       reject(new Error('Timeout fetching ' + u.href)); |       reject(new Error('Timeout fetching ' + u.href)); | ||||||
|     }; |     }; | ||||||
|     let timer = setTimeout(onTimeout, timeout); |     let timer = setTimeout(onTimeout, timeout); | ||||||
|     const switchTimer = () => { |  | ||||||
|       clearTimeout(timer); |  | ||||||
|       timer = loadTimeout && setTimeout(onTimeout, loadTimeout); |  | ||||||
|     }; |  | ||||||
|     if (u.protocol === 'file:' && FIREFOX) { // TODO: maybe remove this since FF68+ can't do it anymore
 |  | ||||||
|       // https://stackoverflow.com/questions/42108782/firefox-webextensions-get-local-files-content-by-path
 |  | ||||||
|       // FIXME: add FetchController when it is available.
 |  | ||||||
|       fetch(u.href, {mode: 'same-origin'}) |  | ||||||
|         .then(r => { |  | ||||||
|           switchTimer(); |  | ||||||
|           return r.status === 200 ? r.text() : Promise.reject(r.status); |  | ||||||
|         }) |  | ||||||
|         .catch(reject) |  | ||||||
|         .then(text => { |  | ||||||
|           clearTimeout(timer); |  | ||||||
|           resolve(text); |  | ||||||
|         }); |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|     xhr = new XMLHttpRequest(); |  | ||||||
|     xhr.onreadystatechange = () => { |     xhr.onreadystatechange = () => { | ||||||
|       if (xhr.readyState >= XMLHttpRequest.HEADERS_RECEIVED) { |       if (xhr.readyState >= XMLHttpRequest.HEADERS_RECEIVED) { | ||||||
|         xhr.onreadystatechange = null; |         xhr.onreadystatechange = null; | ||||||
|         switchTimer(); |         clearTimeout(timer); | ||||||
|  |         timer = loadTimeout && setTimeout(onTimeout, loadTimeout); | ||||||
|       } |       } | ||||||
|     }; |     }; | ||||||
|     xhr.onloadend = event => { |     xhr.onload = () => | ||||||
|       clearTimeout(timer); |       xhr.status === requiredStatusCode || !requiredStatusCode || u.protocol === 'file:' | ||||||
|       if (event.type !== 'error' && ( |         ? resolve(expandUsoVars(xhr.response)) | ||||||
|           xhr.status === requiredStatusCode || !requiredStatusCode || |         : reject(xhr.status); | ||||||
|           u.protocol === 'file:')) { |     xhr.onerror = () => reject(xhr.status); | ||||||
|         resolve(expandUsoVars(xhr.response)); |     xhr.onloadend = () => clearTimeout(timer); | ||||||
|       } else { |  | ||||||
|         reject(xhr.status); |  | ||||||
|       } |  | ||||||
|     }; |  | ||||||
|     xhr.onerror = xhr.onloadend; |  | ||||||
|     xhr.responseType = responseType; |     xhr.responseType = responseType; | ||||||
|     xhr.open(method, u.href, true); |     xhr.open(method, u.href); | ||||||
|     for (const key in headers) { |     for (const [name, value] of Object.entries(headers || {})) { | ||||||
|       xhr.setRequestHeader(key, headers[key]); |       xhr.setRequestHeader(name, value); | ||||||
|     } |     } | ||||||
|     xhr.send(body); |     xhr.send(body); | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
							
								
								
									
										47
									
								
								js/msg.js
									
									
									
									
									
								
							
							
						
						
									
										47
									
								
								js/msg.js
									
									
									
									
									
								
							|  | @ -130,27 +130,30 @@ window.INJECTED !== 1 && (() => { | ||||||
|     }, |     }, | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   window.API = new Proxy({}, { |   const apiHandler = !isBg && { | ||||||
|     get(target, name) { |     get({PATH}, name) { | ||||||
|       // using a named function for convenience when debugging
 |       const fn = () => {}; | ||||||
|       return async function invokeAPI(...args) { |       fn.PATH = [...PATH, name]; | ||||||
|         if (!bg && chrome.tabs) { |       return new Proxy(fn, apiHandler); | ||||||
|           bg = await browser.runtime.getBackgroundPage().catch(() => {}); |  | ||||||
|         } |  | ||||||
|         const message = {method: 'invokeAPI', name, args}; |  | ||||||
|         // content scripts and probably private tabs
 |  | ||||||
|         if (!bg) { |  | ||||||
|           return msg.send(message); |  | ||||||
|         } |  | ||||||
|         // in FF, the object would become a dead object when the window
 |  | ||||||
|         // is closed, so we have to clone the object into background.
 |  | ||||||
|         const res = bg.msg._execute(TARGETS.extension, bg.deepCopy(message), { |  | ||||||
|           frameId: 0, // false in case of our Options frame but we really want to fetch styles early
 |  | ||||||
|           tab: NEEDS_TAB_IN_SENDER.includes(name) && await getOwnTab(), |  | ||||||
|           url: location.href, |  | ||||||
|         }); |  | ||||||
|         return deepCopy(await res); |  | ||||||
|       }; |  | ||||||
|     }, |     }, | ||||||
|   }); |     async apply({PATH: path}, thisObj, args) { | ||||||
|  |       if (!bg && chrome.tabs) { | ||||||
|  |         bg = await browser.runtime.getBackgroundPage().catch(() => {}); | ||||||
|  |       } | ||||||
|  |       const message = {method: 'invokeAPI', path, args}; | ||||||
|  |       // content scripts and probably private tabs
 | ||||||
|  |       if (!bg) { | ||||||
|  |         return msg.send(message); | ||||||
|  |       } | ||||||
|  |       // in FF, the object would become a dead object when the window
 | ||||||
|  |       // is closed, so we have to clone the object into background.
 | ||||||
|  |       const res = bg.msg._execute(TARGETS.extension, bg.deepCopy(message), { | ||||||
|  |         frameId: 0, // false in case of our Options frame but we really want to fetch styles early
 | ||||||
|  |         tab: NEEDS_TAB_IN_SENDER.includes(path.join('.')) && await getOwnTab(), | ||||||
|  |         url: location.href, | ||||||
|  |       }); | ||||||
|  |       return deepCopy(await res); | ||||||
|  |     }, | ||||||
|  |   }; | ||||||
|  |   window.API = isBg ? {} : new Proxy({PATH: []}, apiHandler); | ||||||
| })(); | })(); | ||||||
|  |  | ||||||
|  | @ -6,7 +6,7 @@ | ||||||
| window.INJECTED !== 1 && (() => { | window.INJECTED !== 1 && (() => { | ||||||
|   const STORAGE_KEY = 'settings'; |   const STORAGE_KEY = 'settings'; | ||||||
|   const clone = msg.isBg ? deepCopy : (val => JSON.parse(JSON.stringify(val))); |   const clone = msg.isBg ? deepCopy : (val => JSON.parse(JSON.stringify(val))); | ||||||
|   const defaults = { |   const defaults = /** @namespace Prefs */{ | ||||||
|     'openEditInWindow': false,      // new editor opens in a own browser window
 |     'openEditInWindow': false,      // new editor opens in a own browser window
 | ||||||
|     'openEditInWindow.popup': false, // new editor opens in a simplified browser window without omnibox
 |     'openEditInWindow.popup': false, // new editor opens in a simplified browser window without omnibox
 | ||||||
|     'windowPosition': {},           // detached window position
 |     'windowPosition': {},           // detached window position
 | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| /* global backgroundWorker */ | /* global API */ | ||||||
| /* exported usercss */ | /* exported usercss */ | ||||||
| 'use strict'; | 'use strict'; | ||||||
| 
 | 
 | ||||||
|  | @ -33,7 +33,7 @@ const usercss = (() => { | ||||||
|       throw new Error('can not find metadata'); |       throw new Error('can not find metadata'); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return backgroundWorker.parseUsercssMeta(match[0], match.index) |     return API.worker.parseUsercssMeta(match[0], match.index) | ||||||
|       .catch(err => { |       .catch(err => { | ||||||
|         if (err.code) { |         if (err.code) { | ||||||
|           const args = ERR_ARGS_IS_LIST.has(err.code) ? drawList(err.args) : err.args; |           const args = ERR_ARGS_IS_LIST.has(err.code) ? drawList(err.args) : err.args; | ||||||
|  | @ -68,7 +68,7 @@ const usercss = (() => { | ||||||
|    */ |    */ | ||||||
|   function buildCode(style, allowErrors) { |   function buildCode(style, allowErrors) { | ||||||
|     const match = style.sourceCode.match(RX_META); |     const match = style.sourceCode.match(RX_META); | ||||||
|     return backgroundWorker.compileUsercss( |     return API.worker.compileUsercss( | ||||||
|       style.usercssData.preprocessor, |       style.usercssData.preprocessor, | ||||||
|       style.sourceCode.slice(0, match.index) + style.sourceCode.slice(match.index + match[0].length), |       style.sourceCode.slice(0, match.index) + style.sourceCode.slice(match.index + match[0].length), | ||||||
|       style.usercssData.vars |       style.usercssData.vars | ||||||
|  | @ -95,7 +95,7 @@ const usercss = (() => { | ||||||
|         vars[key].value = oldVars[key].value; |         vars[key].value = oldVars[key].value; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|     return backgroundWorker.nullifyInvalidVars(vars) |     return API.worker.nullifyInvalidVars(vars) | ||||||
|       .then(vars => { |       .then(vars => { | ||||||
|         style.usercssData.vars = vars; |         style.usercssData.vars = vars; | ||||||
|       }); |       }); | ||||||
|  |  | ||||||
|  | @ -128,7 +128,7 @@ function configDialog(style) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|     if (!bgStyle) { |     if (!bgStyle) { | ||||||
|       API.getStyle(style.id, true) |       API.styles.get(style.id) | ||||||
|         .catch(() => ({})) |         .catch(() => ({})) | ||||||
|         .then(bgStyle => save({anyChangeIsDirty}, bgStyle)); |         .then(bgStyle => save({anyChangeIsDirty}, bgStyle)); | ||||||
|       return; |       return; | ||||||
|  | @ -182,7 +182,7 @@ function configDialog(style) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|     saving = true; |     saving = true; | ||||||
|     return API.configUsercssVars(style.id, style.usercssData.vars) |     return API.usercss.configVars(style.id, style.usercssData.vars) | ||||||
|       .then(newVars => { |       .then(newVars => { | ||||||
|         varsInitial = getInitialValues(newVars); |         varsInitial = getInitialValues(newVars); | ||||||
|         vars.forEach(va => onchange({target: va.input, justSaved: true})); |         vars.forEach(va => onchange({target: va.input, justSaved: true})); | ||||||
|  |  | ||||||
|  | @ -109,7 +109,7 @@ function importFromFile({fileTypeFilter, file} = {}) { | ||||||
| 
 | 
 | ||||||
| async function importFromString(jsonString) { | async function importFromString(jsonString) { | ||||||
|   const json = tryJSONparse(jsonString); |   const json = tryJSONparse(jsonString); | ||||||
|   const oldStyles = Array.isArray(json) && json.length ? await API.getAllStyles() : []; |   const oldStyles = Array.isArray(json) && json.length ? await API.styles.getAll() : []; | ||||||
|   const oldStylesById = new Map(oldStyles.map(style => [style.id, style])); |   const oldStylesById = new Map(oldStyles.map(style => [style.id, style])); | ||||||
|   const oldStylesByName = new Map(oldStyles.map(style => [style.name.trim(), style])); |   const oldStylesByName = new Map(oldStyles.map(style => [style.name.trim(), style])); | ||||||
|   const items = []; |   const items = []; | ||||||
|  | @ -126,7 +126,7 @@ async function importFromString(jsonString) { | ||||||
|   await Promise.all(json.map(analyze)); |   await Promise.all(json.map(analyze)); | ||||||
|   bulkChangeQueue.length = 0; |   bulkChangeQueue.length = 0; | ||||||
|   bulkChangeQueue.time = performance.now(); |   bulkChangeQueue.time = performance.now(); | ||||||
|   (await API.importManyStyles(items)) |   (await API.styles.importMany(items)) | ||||||
|     .forEach((style, i) => updateStats(style, infos[i])); |     .forEach((style, i) => updateStats(style, infos[i])); | ||||||
|   return done(); |   return done(); | ||||||
| 
 | 
 | ||||||
|  | @ -290,10 +290,10 @@ async function importFromString(jsonString) { | ||||||
|     ]; |     ]; | ||||||
|     let tasks = Promise.resolve(); |     let tasks = Promise.resolve(); | ||||||
|     for (const id of newIds) { |     for (const id of newIds) { | ||||||
|       tasks = tasks.then(() => API.deleteStyle(id)); |       tasks = tasks.then(() => API.styles.delete(id)); | ||||||
|       const oldStyle = oldStylesById.get(id); |       const oldStyle = oldStylesById.get(id); | ||||||
|       if (oldStyle) { |       if (oldStyle) { | ||||||
|         tasks = tasks.then(() => API.importStyle(oldStyle)); |         tasks = tasks.then(() => API.styles.import(oldStyle)); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|     // taskUI is superfast and updates style list only in this page,
 |     // taskUI is superfast and updates style list only in this page,
 | ||||||
|  | @ -338,7 +338,7 @@ async function exportToFile() { | ||||||
|     Object.assign({ |     Object.assign({ | ||||||
|       [prefs.STORAGE_KEY]: prefs.values, |       [prefs.STORAGE_KEY]: prefs.values, | ||||||
|     }, await chromeSync.getLZValues()), |     }, await chromeSync.getLZValues()), | ||||||
|     ...await API.getAllStyles(), |     ...await API.styles.getAll(), | ||||||
|   ]; |   ]; | ||||||
|   const text = JSON.stringify(data, null, '  '); |   const text = JSON.stringify(data, null, '  '); | ||||||
|   const type = 'application/json'; |   const type = 'application/json'; | ||||||
|  |  | ||||||
|  | @ -94,7 +94,7 @@ const handleEvent = {}; | ||||||
| (async () => { | (async () => { | ||||||
|   const query = router.getSearch('search'); |   const query = router.getSearch('search'); | ||||||
|   const [styles, ids, el] = await Promise.all([ |   const [styles, ids, el] = await Promise.all([ | ||||||
|     API.getAllStyles(), |     API.styles.getAll(), | ||||||
|     query && API.searchDB({query, mode: router.getSearch('searchMode')}), |     query && API.searchDB({query, mode: router.getSearch('searchMode')}), | ||||||
|     waitForSelector('#installed'), // needed to avoid flicker due to an extra frame and layout shift
 |     waitForSelector('#installed'), // needed to avoid flicker due to an extra frame and layout shift
 | ||||||
|     prefs.initializing, |     prefs.initializing, | ||||||
|  | @ -469,7 +469,7 @@ Object.assign(handleEvent, { | ||||||
|   }, |   }, | ||||||
| 
 | 
 | ||||||
|   toggle(event, entry) { |   toggle(event, entry) { | ||||||
|     API.toggleStyle(entry.styleId, this.matches('.enable') || this.checked); |     API.styles.toggle(entry.styleId, this.matches('.enable') || this.checked); | ||||||
|   }, |   }, | ||||||
| 
 | 
 | ||||||
|   check(event, entry) { |   check(event, entry) { | ||||||
|  | @ -481,7 +481,7 @@ Object.assign(handleEvent, { | ||||||
|     event.preventDefault(); |     event.preventDefault(); | ||||||
|     const json = entry.updatedCode; |     const json = entry.updatedCode; | ||||||
|     json.id = entry.styleId; |     json.id = entry.styleId; | ||||||
|     API[json.usercssData ? 'installUsercss' : 'installStyle'](json); |     (json.usercssData ? API.usercss : API.styles).install(json); | ||||||
|   }, |   }, | ||||||
| 
 | 
 | ||||||
|   delete(event, entry) { |   delete(event, entry) { | ||||||
|  | @ -496,7 +496,7 @@ Object.assign(handleEvent, { | ||||||
|     }) |     }) | ||||||
|     .then(({button}) => { |     .then(({button}) => { | ||||||
|       if (button === 0) { |       if (button === 0) { | ||||||
|         API.deleteStyle(id); |         API.styles.delete(id); | ||||||
|       } |       } | ||||||
|     }); |     }); | ||||||
|     const deleteButton = $('#message-box-buttons > button'); |     const deleteButton = $('#message-box-buttons > button'); | ||||||
|  | @ -599,7 +599,7 @@ function handleBulkChange() { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function handleUpdateForId(id, opts) { | function handleUpdateForId(id, opts) { | ||||||
|   return API.getStyle(id).then(style => { |   return API.styles.get(id).then(style => { | ||||||
|     handleUpdate(style, opts); |     handleUpdate(style, opts); | ||||||
|     bulkChangeQueue.time = performance.now(); |     bulkChangeQueue.time = performance.now(); | ||||||
|   }); |   }); | ||||||
|  | @ -697,7 +697,7 @@ function switchUI({styleOnly} = {}) { | ||||||
|   let iconsMissing = iconsEnabled && !$('.applies-to img'); |   let iconsMissing = iconsEnabled && !$('.applies-to img'); | ||||||
|   if (changed.enabled || (iconsMissing && !createStyleElement.parts)) { |   if (changed.enabled || (iconsMissing && !createStyleElement.parts)) { | ||||||
|     installed.textContent = ''; |     installed.textContent = ''; | ||||||
|     API.getAllStyles().then(showStyles); |     API.styles.getAll().then(showStyles); | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|   if (changed.sliders && newUI.enabled) { |   if (changed.sliders && newUI.enabled) { | ||||||
|  |  | ||||||
|  | @ -53,7 +53,7 @@ function checkUpdateAll() { | ||||||
|     chrome.runtime.onConnect.removeListener(onConnect); |     chrome.runtime.onConnect.removeListener(onConnect); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   API.updateCheckAll({ |   API.updater.checkAllStyles({ | ||||||
|     save: false, |     save: false, | ||||||
|     observe: true, |     observe: true, | ||||||
|     ignoreDigest, |     ignoreDigest, | ||||||
|  | @ -98,7 +98,7 @@ function checkUpdate(entry, {single} = {}) { | ||||||
|   $('.update-note', entry).textContent = t('checkingForUpdate'); |   $('.update-note', entry).textContent = t('checkingForUpdate'); | ||||||
|   $('.check-update', entry).title = ''; |   $('.check-update', entry).title = ''; | ||||||
|   if (single) { |   if (single) { | ||||||
|     API.updateCheck({ |     API.updater.checkStyle({ | ||||||
|       save: false, |       save: false, | ||||||
|       id: entry.styleId, |       id: entry.styleId, | ||||||
|       ignoreDigest: entry.classList.contains('update-problem'), |       ignoreDigest: entry.classList.contains('update-problem'), | ||||||
|  | @ -221,7 +221,7 @@ function showUpdateHistory(event) { | ||||||
|   let deleted = false; |   let deleted = false; | ||||||
|   Promise.all([ |   Promise.all([ | ||||||
|     chromeLocal.getValue('updateLog'), |     chromeLocal.getValue('updateLog'), | ||||||
|     API.getUpdaterStates(), |     API.updater.getStates(), | ||||||
|   ]).then(([lines = [], states]) => { |   ]).then(([lines = [], states]) => { | ||||||
|     logText = lines.join('\n'); |     logText = lines.join('\n'); | ||||||
|     messageBox({ |     messageBox({ | ||||||
|  |  | ||||||
|  | @ -52,12 +52,13 @@ | ||||||
|       "background/tab-manager.js", |       "background/tab-manager.js", | ||||||
|       "background/icon-manager.js", |       "background/icon-manager.js", | ||||||
|       "background/background.js", |       "background/background.js", | ||||||
|       "background/usercss-helper.js", |       "background/usercss-api-helper.js", | ||||||
|       "background/usercss-install-helper.js", |       "background/usercss-install-helper.js", | ||||||
|       "background/style-via-api.js", |       "background/style-via-api.js", | ||||||
|       "background/style-via-webrequest.js", |       "background/style-via-webrequest.js", | ||||||
|       "background/search-db.js", |       "background/search-db.js", | ||||||
|       "background/update.js", |       "background/update.js", | ||||||
|  |       "background/context-menus.js", | ||||||
|       "background/openusercss-api.js" |       "background/openusercss-api.js" | ||||||
|     ] |     ] | ||||||
|   }, |   }, | ||||||
|  |  | ||||||
|  | @ -1,7 +1,25 @@ | ||||||
| /* global messageBox msg setupLivePrefs enforceInputRange | /* global | ||||||
|   $ $$ $create $createLink |   $ | ||||||
|   FIREFOX OPERA CHROME URLS openURL prefs t API ignoreChromeError |   $$ | ||||||
|   CHROME_HAS_BORDER_BUG capitalize */ |   $create | ||||||
|  |   $createLink | ||||||
|  |   API | ||||||
|  |   capitalize | ||||||
|  |   CHROME | ||||||
|  |   CHROME_HAS_BORDER_BUG | ||||||
|  |   enforceInputRange | ||||||
|  |   FIREFOX | ||||||
|  |   getEventKeyName | ||||||
|  |   ignoreChromeError | ||||||
|  |   messageBox | ||||||
|  |   msg | ||||||
|  |   openURL | ||||||
|  |   OPERA | ||||||
|  |   prefs | ||||||
|  |   setupLivePrefs | ||||||
|  |   t | ||||||
|  |   URLS | ||||||
|  | */ | ||||||
| 'use strict'; | 'use strict'; | ||||||
| 
 | 
 | ||||||
| setupLivePrefs(); | setupLivePrefs(); | ||||||
|  | @ -44,7 +62,7 @@ if (CHROME && !chrome.declarativeContent) { | ||||||
|   prefs.initializing.then(() => { |   prefs.initializing.then(() => { | ||||||
|     el.checked = false; |     el.checked = false; | ||||||
|   }); |   }); | ||||||
|   el.addEventListener('click', () => { |   el.on('click', () => { | ||||||
|     if (el.checked) { |     if (el.checked) { | ||||||
|       chrome.permissions.request({permissions: ['declarativeContent']}, ignoreChromeError); |       chrome.permissions.request({permissions: ['declarativeContent']}, ignoreChromeError); | ||||||
|     } |     } | ||||||
|  | @ -101,84 +119,75 @@ document.onclick = e => { | ||||||
| 
 | 
 | ||||||
| // sync to cloud
 | // sync to cloud
 | ||||||
| (() => { | (() => { | ||||||
|   const cloud = document.querySelector('.sync-options .cloud-name'); |   const elCloud = $('.sync-options .cloud-name'); | ||||||
|   const connectButton = document.querySelector('.sync-options .connect'); |   const elStart = $('.sync-options .connect'); | ||||||
|   const disconnectButton = document.querySelector('.sync-options .disconnect'); |   const elStop = $('.sync-options .disconnect'); | ||||||
|   const syncButton = document.querySelector('.sync-options .sync-now'); |   const elSyncNow = $('.sync-options .sync-now'); | ||||||
|   const statusText = document.querySelector('.sync-options .sync-status'); |   const elStatus = $('.sync-options .sync-status'); | ||||||
|   const loginButton = document.querySelector('.sync-options .sync-login'); |   const elLogin = $('.sync-options .sync-login'); | ||||||
| 
 |   /** @type {API.sync.Status} */ | ||||||
|   let status = {}; |   let status = {}; | ||||||
| 
 |  | ||||||
|   msg.onExtension(e => { |   msg.onExtension(e => { | ||||||
|     if (e.method === 'syncStatusUpdate') { |     if (e.method === 'syncStatusUpdate') { | ||||||
|       status = e.status; |       setStatus(e.status); | ||||||
|       updateButtons(); |  | ||||||
|     } |     } | ||||||
|   }); |   }); | ||||||
|  |   API.sync.getStatus() | ||||||
|  |     .then(setStatus); | ||||||
| 
 | 
 | ||||||
|   API.getSyncStatus() |   elCloud.on('change', updateButtons); | ||||||
|     .then(_status => { |   for (const [btn, fn] of [ | ||||||
|       status = _status; |     [elStart, () => API.sync.start(elCloud.value)], | ||||||
|       updateButtons(); |     [elStop, API.sync.stop], | ||||||
|  |     [elSyncNow, API.sync.syncNow], | ||||||
|  |     [elLogin, API.sync.login], | ||||||
|  |   ]) { | ||||||
|  |     btn.on('click', e => { | ||||||
|  |       if (getEventKeyName(e) === 'L') { | ||||||
|  |         fn(); | ||||||
|  |       } | ||||||
|     }); |     }); | ||||||
| 
 |  | ||||||
|   function validClick(e) { |  | ||||||
|     return e.button === 0 && !e.ctrl && !e.alt && !e.shift; |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   cloud.addEventListener('change', updateButtons); |   function setStatus(newStatus) { | ||||||
|  |     status = newStatus; | ||||||
|  |     updateButtons(); | ||||||
|  |   } | ||||||
| 
 | 
 | ||||||
|   function updateButtons() { |   function updateButtons() { | ||||||
|  |     const isConnected = status.state === 'connected'; | ||||||
|  |     const isDisconnected = status.state === 'disconnected'; | ||||||
|     if (status.currentDriveName) { |     if (status.currentDriveName) { | ||||||
|       cloud.value = status.currentDriveName; |       elCloud.value = status.currentDriveName; | ||||||
|     } |     } | ||||||
|     cloud.disabled = status.state !== 'disconnected'; |     for (const [el, enable] of [ | ||||||
|     connectButton.disabled = status.state !== 'disconnected' || cloud.value === 'none'; |       [elCloud, isDisconnected], | ||||||
|     disconnectButton.disabled = status.state !== 'connected' || status.syncing; |       [elStart, isDisconnected && elCloud.value !== 'none'], | ||||||
|     syncButton.disabled = status.state !== 'connected' || status.syncing; |       [elStop, isConnected && !status.syncing], | ||||||
|     statusText.textContent = getStatusText(); |       [elSyncNow, isConnected && !status.syncing], | ||||||
|     loginButton.style.display = status.state === 'connected' && !status.login ? '' : 'none'; |     ]) { | ||||||
|  |       el.disabled = !enable; | ||||||
|  |     } | ||||||
|  |     elStatus.textContent = getStatusText(); | ||||||
|  |     elLogin.hidden = !isConnected || status.login; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   function getStatusText() { |   function getStatusText() { | ||||||
|  |     // chrome.i18n.getMessage is used instead of t() because calculated ids may be absent
 | ||||||
|  |     let res; | ||||||
|     if (status.syncing) { |     if (status.syncing) { | ||||||
|       if (status.progress) { |       const {phase, loaded, total} = status.progress || {}; | ||||||
|         const {phase, loaded, total} = status.progress; |       res = phase | ||||||
|         return chrome.i18n.getMessage(`optionsSyncStatus${capitalize(phase)}`, [loaded + 1, total]) || |         ? chrome.i18n.getMessage(`optionsSyncStatus${capitalize(phase)}`, [loaded + 1, total]) || | ||||||
|           `${phase} ${loaded} / ${total}`; |           `${phase} ${loaded} / ${total}` | ||||||
|       } |         : t('optionsSyncStatusSyncing'); | ||||||
|       return chrome.i18n.getMessage('optionsSyncStatusSyncing') || 'syncing'; |     } else { | ||||||
|  |       const {state, errorMessage} = status; | ||||||
|  |       res = (state === 'connected' || state === 'disconnected') && errorMessage || | ||||||
|  |         chrome.i18n.getMessage(`optionsSyncStatus${capitalize(state)}`) || state; | ||||||
|     } |     } | ||||||
|     if ((status.state === 'connected' || status.state === 'disconnected') && status.errorMessage) { |     return res; | ||||||
|       return status.errorMessage; |  | ||||||
|     } |  | ||||||
|     return chrome.i18n.getMessage(`optionsSyncStatus${capitalize(status.state)}`) || status.state; |  | ||||||
|   } |   } | ||||||
| 
 |  | ||||||
|   connectButton.addEventListener('click', e => { |  | ||||||
|     if (validClick(e)) { |  | ||||||
|       API.syncStart(cloud.value).catch(console.error); |  | ||||||
|     } |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   disconnectButton.addEventListener('click', e => { |  | ||||||
|     if (validClick(e)) { |  | ||||||
|       API.syncStop().catch(console.error); |  | ||||||
|     } |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   syncButton.addEventListener('click', e => { |  | ||||||
|     if (validClick(e)) { |  | ||||||
|       API.syncNow().catch(console.error); |  | ||||||
|     } |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   loginButton.addEventListener('click', e => { |  | ||||||
|     if (validClick(e)) { |  | ||||||
|       API.syncLogin().catch(console.error); |  | ||||||
|     } |  | ||||||
|   }); |  | ||||||
| })(); | })(); | ||||||
| 
 | 
 | ||||||
| function checkUpdates() { | function checkUpdates() { | ||||||
|  | @ -193,7 +202,7 @@ function checkUpdates() { | ||||||
|     chrome.runtime.onConnect.removeListener(onConnect); |     chrome.runtime.onConnect.removeListener(onConnect); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   API.updateCheckAll({observe: true}); |   API.updater.checkAllStyles({observe: true}); | ||||||
| 
 | 
 | ||||||
|   function observer(info) { |   function observer(info) { | ||||||
|     if ('count' in info) { |     if ('count' in info) { | ||||||
|  | @ -223,7 +232,7 @@ function setupRadioButtons() { | ||||||
|   // group all radio-inputs by name="prefName" attribute
 |   // group all radio-inputs by name="prefName" attribute
 | ||||||
|   for (const el of $$('input[type="radio"][name]')) { |   for (const el of $$('input[type="radio"][name]')) { | ||||||
|     (sets[el.name] = sets[el.name] || []).push(el); |     (sets[el.name] = sets[el.name] || []).push(el); | ||||||
|     el.addEventListener('change', onChange); |     el.on('change', onChange); | ||||||
|   } |   } | ||||||
|   // select the input corresponding to the actual pref value
 |   // select the input corresponding to the actual pref value
 | ||||||
|   for (const name in sets) { |   for (const name in sets) { | ||||||
|  |  | ||||||
|  | @ -89,7 +89,7 @@ const hotkeys = (() => { | ||||||
|       if (!match && $('input', entry).checked !== enable || entry.classList.contains(match)) { |       if (!match && $('input', entry).checked !== enable || entry.classList.contains(match)) { | ||||||
|         results.push(entry.id); |         results.push(entry.id); | ||||||
|         task = task |         task = task | ||||||
|           .then(() => API.toggleStyle(entry.styleId, enable)) |           .then(() => API.styles.toggle(entry.styleId, enable)) | ||||||
|           .then(() => { |           .then(() => { | ||||||
|             entry.classList.toggle('enabled', enable); |             entry.classList.toggle('enabled', enable); | ||||||
|             entry.classList.toggle('disabled', !enable); |             entry.classList.toggle('disabled', !enable); | ||||||
|  |  | ||||||
|  | @ -81,7 +81,7 @@ const initializing = (async () => { | ||||||
| /* Merges the extra props from API into style data. | /* Merges the extra props from API into style data. | ||||||
|  * When `id` is specified returns a single object otherwise an array */ |  * When `id` is specified returns a single object otherwise an array */ | ||||||
| async function getStyleDataMerged(url, id) { | async function getStyleDataMerged(url, id) { | ||||||
|   const styles = (await API.getStylesByUrl(url, id)) |   const styles = (await API.styles.getByUrl(url, id)) | ||||||
|     .map(r => Object.assign(r.data, r)); |     .map(r => Object.assign(r.style, r)); | ||||||
|   return id ? styles[0] : styles; |   return id ? styles[0] : styles; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -143,7 +143,7 @@ async function initPopup(frames) { | ||||||
|     switch (e.target.dataset.cmd) { |     switch (e.target.dataset.cmd) { | ||||||
|       case 'ok': |       case 'ok': | ||||||
|         hideModal(this, {animate: true}); |         hideModal(this, {animate: true}); | ||||||
|         API.deleteStyle(Number(id)); |         API.styles.delete(Number(id)); | ||||||
|         break; |         break; | ||||||
|       case 'cancel': |       case 'cancel': | ||||||
|         showModal($('.menu', $.entry(id)), '.menu-close'); |         showModal($('.menu', $.entry(id)), '.menu-close'); | ||||||
|  | @ -464,20 +464,19 @@ Object.assign(handleEvent, { | ||||||
|     event.preventDefault(); |     event.preventDefault(); | ||||||
|   }, |   }, | ||||||
| 
 | 
 | ||||||
|   toggle(event) { |   async toggle(event) { | ||||||
|     // when fired on checkbox, prevent the parent label from seeing the event, see #501
 |     // when fired on checkbox, prevent the parent label from seeing the event, see #501
 | ||||||
|     event.stopPropagation(); |     event.stopPropagation(); | ||||||
|     API |     await API.styles.toggle(handleEvent.getClickedStyleId(event), this.checked); | ||||||
|       .toggleStyle(handleEvent.getClickedStyleId(event), this.checked) |     resortEntries(); | ||||||
|       .then(() => resortEntries()); |  | ||||||
|   }, |   }, | ||||||
| 
 | 
 | ||||||
|   toggleExclude(event, type) { |   toggleExclude(event, type) { | ||||||
|     const entry = handleEvent.getClickedStyleElement(event); |     const entry = handleEvent.getClickedStyleElement(event); | ||||||
|     if (event.target.checked) { |     if (event.target.checked) { | ||||||
|       API.addExclusion(entry.styleMeta.id, getExcludeRule(type)); |       API.styles.addExclusion(entry.styleMeta.id, getExcludeRule(type)); | ||||||
|     } else { |     } else { | ||||||
|       API.removeExclusion(entry.styleMeta.id, getExcludeRule(type)); |       API.styles.removeExclusion(entry.styleMeta.id, getExcludeRule(type)); | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
| 
 | 
 | ||||||
|  | @ -503,7 +502,7 @@ Object.assign(handleEvent, { | ||||||
|   configure(event) { |   configure(event) { | ||||||
|     const {styleId, styleIsUsercss} = handleEvent.getClickedStyleElement(event); |     const {styleId, styleIsUsercss} = handleEvent.getClickedStyleElement(event); | ||||||
|     if (styleIsUsercss) { |     if (styleIsUsercss) { | ||||||
|       API.getStyle(styleId, true).then(style => { |       API.styles.get(styleId).then(style => { | ||||||
|         hotkeys.setState(false); |         hotkeys.setState(false); | ||||||
|         configDialog(style).then(() => { |         configDialog(style).then(() => { | ||||||
|           hotkeys.setState(true); |           hotkeys.setState(true); | ||||||
|  |  | ||||||
|  | @ -149,7 +149,7 @@ window.addEventListener('showStyles:done', () => { | ||||||
|     addEventListener('styleAdded', async ({detail: {style}}) => { |     addEventListener('styleAdded', async ({detail: {style}}) => { | ||||||
|       restoreScrollPosition(); |       restoreScrollPosition(); | ||||||
|       const usoId = calcUsoId(style) || |       const usoId = calcUsoId(style) || | ||||||
|                     calcUsoId(await API.getStyle(style.id, true)); |                     calcUsoId(await API.styles.get(style.id)); | ||||||
|       if (usoId && results.find(r => r.i === usoId)) { |       if (usoId && results.find(r => r.i === usoId)) { | ||||||
|         renderActionButtons(usoId, style.id); |         renderActionButtons(usoId, style.id); | ||||||
|       } |       } | ||||||
|  | @ -194,7 +194,7 @@ window.addEventListener('showStyles:done', () => { | ||||||
|         results = await search({retry}); |         results = await search({retry}); | ||||||
|       } |       } | ||||||
|       if (results.length) { |       if (results.length) { | ||||||
|         const installedStyles = await API.getAllStyles(); |         const installedStyles = await API.styles.getAll(); | ||||||
|         const allUsoIds = new Set(installedStyles.map(calcUsoId)); |         const allUsoIds = new Set(installedStyles.map(calcUsoId)); | ||||||
|         results = results.filter(r => !allUsoIds.has(r.i)); |         results = results.filter(r => !allUsoIds.has(r.i)); | ||||||
|       } |       } | ||||||
|  | @ -419,7 +419,7 @@ window.addEventListener('showStyles:done', () => { | ||||||
|     const updateUrl = `${URLS.usoArchiveRaw}usercss/${id}.user.css`; |     const updateUrl = `${URLS.usoArchiveRaw}usercss/${id}.user.css`; | ||||||
|     try { |     try { | ||||||
|       const sourceCode = await download(updateUrl); |       const sourceCode = await download(updateUrl); | ||||||
|       const style = await API.installUsercss({sourceCode, updateUrl}); |       const style = await API.usercss.install({sourceCode, updateUrl}); | ||||||
|       renderFullInfo(entry, style); |       renderFullInfo(entry, style); | ||||||
|     } catch (reason) { |     } catch (reason) { | ||||||
|       error(`Error while downloading usoID:${id}\nReason: ${reason}`); |       error(`Error while downloading usoID:${id}\nReason: ${reason}`); | ||||||
|  | @ -432,7 +432,7 @@ window.addEventListener('showStyles:done', () => { | ||||||
|   function uninstall() { |   function uninstall() { | ||||||
|     const entry = this.closest('.search-result'); |     const entry = this.closest('.search-result'); | ||||||
|     saveScrollPosition(entry); |     saveScrollPosition(entry); | ||||||
|     API.deleteStyle(entry._result.installedStyleId); |     API.styles.delete(entry._result.installedStyleId); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   function saveScrollPosition(entry) { |   function saveScrollPosition(entry) { | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user