FF: support private/container tabs
This commit is contained in:
		
							parent
							
								
									62e333a0ba
								
							
						
					
					
						commit
						3418ac9cb9
					
				
							
								
								
									
										10
									
								
								.eslintrc
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								.eslintrc
									
									
									
									
									
								
							|  | @ -13,9 +13,11 @@ globals: | |||
|   KEEP_CHANNEL_OPEN: false | ||||
|   CHROME: false | ||||
|   FIREFOX: false | ||||
|   VIVALDI: false | ||||
|   OPERA: false | ||||
|   URLS: false | ||||
|   BG: false | ||||
|   API: false | ||||
|   notifyAllTabs: false | ||||
|   sendMessage: false | ||||
|   queryTabs: false | ||||
|  | @ -33,10 +35,6 @@ globals: | |||
|   tryJSONparse: false | ||||
|   debounce: false | ||||
|   deepCopy: false | ||||
|   onBackgroundReady: false | ||||
|   deleteStyleSafe: false | ||||
|   getStylesSafe: false | ||||
|   saveStyleSafe: false | ||||
|   sessionStorageHash: false | ||||
|   download: false | ||||
|   invokeOrPostpone: false | ||||
|  | @ -63,6 +61,10 @@ globals: | |||
|   # prefs.js | ||||
|   prefs: false | ||||
|   setupLivePrefs: false | ||||
|   # storage-util.js | ||||
|   chromeLocal: false | ||||
|   chromeSync: false | ||||
|   LZString: false | ||||
| 
 | ||||
| rules: | ||||
|   accessor-pairs: [2] | ||||
|  |  | |||
|  | @ -1,9 +1,38 @@ | |||
| /* global dbExec, getStyles, saveStyle */ | ||||
| /* global handleCssTransitionBug */ | ||||
| /* global usercssHelper openEditor */ | ||||
| /* global styleViaAPI */ | ||||
| /* | ||||
|  global dbExec getStyles saveStyle deleteStyle | ||||
|  global handleCssTransitionBug detectSloppyRegexps | ||||
|  global openEditor | ||||
|  global styleViaAPI | ||||
|  global loadScript | ||||
|  global updater | ||||
|  */ | ||||
| 'use strict'; | ||||
| 
 | ||||
| // eslint-disable-next-line no-var
 | ||||
| var API_METHODS = { | ||||
| 
 | ||||
|   getStyles, | ||||
|   saveStyle, | ||||
|   deleteStyle, | ||||
| 
 | ||||
|   download:    msg => download(msg.url), | ||||
|   getPrefs:    () => prefs.getAll(), | ||||
|   healthCheck: () => dbExec().then(() => true), | ||||
| 
 | ||||
|   detectSloppyRegexps, | ||||
|   openEditor, | ||||
|   updateIcon, | ||||
| 
 | ||||
|   closeTab: (msg, sender, respond) => { | ||||
|     chrome.tabs.remove(msg.tabId || sender.tab.id, () => { | ||||
|       if (chrome.runtime.lastError && msg.tabId !== sender.tab.id) { | ||||
|         respond(new Error(chrome.runtime.lastError.message)); | ||||
|       } | ||||
|     }); | ||||
|     return KEEP_CHANNEL_OPEN; | ||||
|   }, | ||||
| }; | ||||
| 
 | ||||
| // eslint-disable-next-line no-var
 | ||||
| var browserCommands, contextMenus; | ||||
| 
 | ||||
|  | @ -55,9 +84,17 @@ if (!chrome.browserAction || | |||
|   window.updateIcon = () => {}; | ||||
| } | ||||
| 
 | ||||
| const tabIcons = new Map(); | ||||
| chrome.tabs.onRemoved.addListener(tabId => tabIcons.delete(tabId)); | ||||
| chrome.tabs.onReplaced.addListener((added, removed) => tabIcons.delete(removed)); | ||||
| 
 | ||||
| // *************************************************************************
 | ||||
| // set the default icon displayed after a tab is created until webNavigation kicks in
 | ||||
| prefs.subscribe(['iconset'], () => updateIcon({id: undefined}, {})); | ||||
| prefs.subscribe(['iconset'], () => | ||||
|   updateIcon({ | ||||
|     tab: {id: undefined}, | ||||
|     styles: {}, | ||||
|   })); | ||||
| 
 | ||||
| // *************************************************************************
 | ||||
| { | ||||
|  | @ -160,7 +197,10 @@ if (chrome.contextMenus) { | |||
| window.addEventListener('storageReady', function _() { | ||||
|   window.removeEventListener('storageReady', _); | ||||
| 
 | ||||
|   updateIcon({id: undefined}, {}); | ||||
|   updateIcon({ | ||||
|     tab: {id: undefined}, | ||||
|     styles: {}, | ||||
|   }); | ||||
| 
 | ||||
|   const NTP = 'chrome://newtab/'; | ||||
|   const ALL_URLS = '<all_urls>'; | ||||
|  | @ -223,7 +263,8 @@ function webNavigationListener(method, {url, tabId, frameId}) { | |||
|     } | ||||
|     // main page frame id is 0
 | ||||
|     if (frameId === 0) { | ||||
|       updateIcon({id: tabId, url}, styles); | ||||
|       tabIcons.delete(tabId); | ||||
|       updateIcon({tab: {id: tabId, url}, styles}); | ||||
|     } | ||||
|   }); | ||||
| } | ||||
|  | @ -256,13 +297,13 @@ function webNavUsercssInstallerFF(data) { | |||
|     getTab(tabId), | ||||
|   ]).then(([pong, tab]) => { | ||||
|     if (pong !== true && tab.url !== 'about:blank') { | ||||
|       usercssHelper.openInstallPage(tab, {direct: true}); | ||||
|       API_METHODS.installUsercss({direct: true}, {tab}); | ||||
|     } | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| function updateIcon(tab, styles) { | ||||
| function updateIcon({tab, styles}) { | ||||
|   if (tab.id < 0) { | ||||
|     return; | ||||
|   } | ||||
|  | @ -277,38 +318,44 @@ function updateIcon(tab, styles) { | |||
|     .then(url => getStyles({matchUrl: url, enabled: true, asHash: true})) | ||||
|     .then(stylesReceived); | ||||
| 
 | ||||
|   function countStyles(styles) { | ||||
|     if (Array.isArray(styles)) return styles.length; | ||||
|     return Object.keys(styles).reduce((sum, id) => sum + !isNaN(Number(id)), 0); | ||||
|   } | ||||
| 
 | ||||
|   function stylesReceived(styles) { | ||||
|     let numStyles = styles.length; | ||||
|     if (numStyles === undefined) { | ||||
|       // for 'styles' asHash:true fake the length by counting numeric ids manually
 | ||||
|       numStyles = 0; | ||||
|       for (const id of Object.keys(styles)) { | ||||
|         numStyles += id.match(/^\d+$/) ? 1 : 0; | ||||
|       } | ||||
|     } | ||||
|     const numStyles = countStyles(styles); | ||||
|     const disableAll = 'disableAll' in styles ? styles.disableAll : prefs.get('disableAll'); | ||||
|     const postfix = disableAll ? 'x' : numStyles === 0 ? 'w' : ''; | ||||
|     const color = prefs.get(disableAll ? 'badgeDisabled' : 'badgeNormal'); | ||||
|     const text = prefs.get('show-badge') && numStyles ? String(numStyles) : ''; | ||||
|     const iconset = ['', 'light/'][prefs.get('iconset')] || ''; | ||||
|     const path = 'images/icon/' + iconset; | ||||
|     chrome.browserAction.setIcon({ | ||||
|       tabId: tab.id, | ||||
|       path: { | ||||
|     const tabIcon = tabIcons.get(tab.id) || {}; | ||||
|     if (tabIcon.iconType !== iconset + postfix) { | ||||
|       tabIcons.set(tab.id, tabIcon); | ||||
|       tabIcon.iconType = iconset + postfix; | ||||
|       const paths = {}; | ||||
|       if (FIREFOX || CHROME >= 2883 && !VIVALDI) { | ||||
|         // Material Design 2016 new size is 16px
 | ||||
|         16: `${path}16${postfix}.png`, | ||||
|         32: `${path}32${postfix}.png`, | ||||
|         paths['16'] = `${path}16${postfix}.png`; | ||||
|         paths['32'] = `${path}32${postfix}.png`; | ||||
|       } else { | ||||
|         // Chromium forks or non-chromium browsers may still use the traditional 19px
 | ||||
|         19: `${path}19${postfix}.png`, | ||||
|         38: `${path}38${postfix}.png`, | ||||
|         // TODO: add Edge preferred sizes: 20, 25, 30, 40
 | ||||
|       }, | ||||
|     }, () => { | ||||
|       if (chrome.runtime.lastError || tab.id === undefined) { | ||||
|         return; | ||||
|         paths['19'] = `${path}19${postfix}.png`; | ||||
|         paths['38'] = `${path}38${postfix}.png`; | ||||
|       } | ||||
|       // Vivaldi bug workaround: setBadgeText must follow setBadgeBackgroundColor
 | ||||
|       chrome.browserAction.setIcon({tabId: tab.id, path: paths}, ignoreChromeError); | ||||
|     } | ||||
|     if (tab.id === undefined) return; | ||||
|     let defaultIcon = tabIcons.get(undefined); | ||||
|     if (!defaultIcon) tabIcons.set(undefined, (defaultIcon = {})); | ||||
|     if (defaultIcon.color !== color) { | ||||
|       defaultIcon.color = color; | ||||
|       chrome.browserAction.setBadgeBackgroundColor({color}); | ||||
|     } | ||||
|     if (tabIcon.text !== text) { | ||||
|       tabIcon.text = text; | ||||
|       setTimeout(() => { | ||||
|         getTab(tab.id).then(realTab => { | ||||
|           // skip pre-rendered tabs
 | ||||
|  | @ -317,67 +364,31 @@ function updateIcon(tab, styles) { | |||
|           } | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| function onRuntimeMessage(request, sender, sendResponseInternal) { | ||||
|   const sendResponse = data => { | ||||
|     // wrap Error object instance as {__ERROR__: message} - will be unwrapped in sendMessage
 | ||||
|     if (data instanceof Error) { | ||||
|       data = {__ERROR__: data.message}; | ||||
|     } | ||||
|     // prevent browser exception bug on sending a response to a closed tab
 | ||||
|     tryCatch(sendResponseInternal, data); | ||||
|   }; | ||||
|   switch (request.method) { | ||||
|     case 'getStyles': | ||||
|       getStyles(request).then(sendResponse); | ||||
|       return KEEP_CHANNEL_OPEN; | ||||
| 
 | ||||
|     case 'saveStyle': | ||||
|       saveStyle(request).then(sendResponse); | ||||
|       return KEEP_CHANNEL_OPEN; | ||||
| 
 | ||||
|     case 'saveUsercss': | ||||
|       usercssHelper.save(request, true).then(sendResponse); | ||||
|       return KEEP_CHANNEL_OPEN; | ||||
| 
 | ||||
|     case 'buildUsercss': | ||||
|       usercssHelper.build(request, true).then(sendResponse); | ||||
|       return KEEP_CHANNEL_OPEN; | ||||
| 
 | ||||
|     case 'healthCheck': | ||||
|       dbExec() | ||||
|         .then(() => sendResponse(true)) | ||||
|         .catch(() => sendResponse(false)); | ||||
|       return KEEP_CHANNEL_OPEN; | ||||
| 
 | ||||
|     case 'styleViaAPI': | ||||
|       styleViaAPI(request, sender); | ||||
|       return; | ||||
| 
 | ||||
|     case 'download': | ||||
|       download(request.url) | ||||
|         .then(sendResponse) | ||||
|         .catch(() => sendResponse(null)); | ||||
|       return KEEP_CHANNEL_OPEN; | ||||
| 
 | ||||
|     case 'openUsercssInstallPage': | ||||
|       usercssHelper.openInstallPage(sender.tab, request).then(sendResponse); | ||||
|       return KEEP_CHANNEL_OPEN; | ||||
| 
 | ||||
|     case 'closeTab': | ||||
|       chrome.tabs.remove(request.tabId || sender.tab.id, () => { | ||||
|         if (chrome.runtime.lastError && request.tabId !== sender.tab.id) { | ||||
|           sendResponse(new Error(chrome.runtime.lastError.message)); | ||||
|         } | ||||
|       }); | ||||
|       return KEEP_CHANNEL_OPEN; | ||||
| 
 | ||||
|     case 'openEditor': | ||||
|       openEditor(request.id); | ||||
|       return; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| function onRuntimeMessage(msg, sender, sendResponse) { | ||||
|   const fn = API_METHODS[msg.method]; | ||||
|   if (!fn) return; | ||||
| 
 | ||||
|   // wrap 'Error' object instance as {__ERROR__: message},
 | ||||
|   // which will be unwrapped by sendMessage,
 | ||||
|   // and prevent exceptions on sending to a closed tab
 | ||||
|   const respond = data => | ||||
|     tryCatch(sendResponse, | ||||
|       data instanceof Error ? {__ERROR__: data.message} : data); | ||||
| 
 | ||||
|   const result = fn(msg, sender, respond); | ||||
|   if (result instanceof Promise) { | ||||
|     result | ||||
|       .catch(e => ({__ERROR__: e instanceof Error ? e.message : e})) | ||||
|       .then(respond); | ||||
|     return KEEP_CHANNEL_OPEN; | ||||
|   } else if (result === KEEP_CHANNEL_OPEN) { | ||||
|     return KEEP_CHANNEL_OPEN; | ||||
|   } else if (result !== undefined) { | ||||
|     respond(result); | ||||
|   } | ||||
| } | ||||
|  |  | |||
							
								
								
									
										100
									
								
								background/search-db.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								background/search-db.js
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,100 @@ | |||
| /* global API_METHODS filterStyles cachedStyles */ | ||||
| 'use strict'; | ||||
| 
 | ||||
| (() => { | ||||
|   // toLocaleLowerCase cache, autocleared after 1 minute
 | ||||
|   const cache = new Map(); | ||||
|   // top-level style properties to be searched
 | ||||
|   const PARTS = { | ||||
|     name: searchText, | ||||
|     url: searchText, | ||||
|     sourceCode: searchText, | ||||
|     sections: searchSections, | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * @param params | ||||
|    * @param {string} params.query - 1. url:someurl 2. text (may contain quoted parts like "qUot Ed") | ||||
|    * @param {number[]} [params.ids] - if not specified, all styles are searched | ||||
|    * @returns {number[]} - array of matched styles ids | ||||
|    */ | ||||
|   API_METHODS.searchDB = ({query, ids}) => { | ||||
|     let rx, words, icase, matchUrl; | ||||
|     query = query.trim(); | ||||
| 
 | ||||
|     if (/^url:/i.test(query)) { | ||||
|       matchUrl = query.slice(query.indexOf(':') + 1).trim(); | ||||
|       if (matchUrl) { | ||||
|         return filterStyles({matchUrl}).map(style => style.id); | ||||
|       } | ||||
|     } | ||||
|     if (query.startsWith('/') && /^\/(.+?)\/([gimsuy]*)$/.test(query)) { | ||||
|       rx = tryRegExp(RegExp.$1, RegExp.$2); | ||||
|     } | ||||
|     if (!rx) { | ||||
|       words = query | ||||
|         .split(/(".*?")|\s+/) | ||||
|         .filter(Boolean) | ||||
|         .map(w => w.startsWith('"') && w.endsWith('"') | ||||
|           ? w.slice(1, -1) | ||||
|           : w) | ||||
|         .filter(w => w.length > 1); | ||||
|       words = words.length ? words : [query]; | ||||
|       icase = words.some(w => w === lower(w)); | ||||
|     } | ||||
| 
 | ||||
|     const results = []; | ||||
|     for (const item of ids || cachedStyles.list) { | ||||
|       const id = isNaN(item) ? item.id : item; | ||||
|       if (!query || words && !words.length) { | ||||
|         results.push(id); | ||||
|         continue; | ||||
|       } | ||||
|       const style = isNaN(item) ? item : cachedStyles.byId.get(item); | ||||
|       if (!style) continue; | ||||
|       for (const part in PARTS) { | ||||
|         const text = style[part]; | ||||
|         if (text && PARTS[part](text, rx, words, icase)) { | ||||
|           results.push(id); | ||||
|           break; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     if (cache.size) debounce(clearCache, 60e3); | ||||
|     return results; | ||||
|   }; | ||||
| 
 | ||||
|   function searchText(text, rx, words, icase) { | ||||
|     if (rx) return rx.test(text); | ||||
|     for (let pass = 1; pass <= (icase ? 2 : 1); pass++) { | ||||
|       if (words.every(w => text.includes(w))) return true; | ||||
|       text = lower(text); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   function searchSections(sections, rx, words, icase) { | ||||
|     for (const section of sections) { | ||||
|       for (const prop in section) { | ||||
|         const value = section[prop]; | ||||
|         if (typeof value === 'string') { | ||||
|           if (searchText(value, rx, words, icase)) return true; | ||||
|         } else if (Array.isArray(value)) { | ||||
|           if (value.some(str => searchText(str, rx, words, icase))) return true; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   function lower(text) { | ||||
|     let result = cache.get(text); | ||||
|     if (result) return result; | ||||
|     result = text.toLocaleLowerCase(); | ||||
|     cache.set(text, result); | ||||
|     return result; | ||||
|   } | ||||
| 
 | ||||
|   function clearCache() { | ||||
|     cache.clear(); | ||||
|   } | ||||
| })(); | ||||
|  | @ -1,4 +1,4 @@ | |||
| /* global LZString */ | ||||
| /* global getStyleWithNoCode */ | ||||
| 'use strict'; | ||||
| 
 | ||||
| const RX_NAMESPACE = new RegExp([/[\s\r\n]*/, | ||||
|  | @ -29,54 +29,6 @@ var cachedStyles = { | |||
|   }, | ||||
| }; | ||||
| 
 | ||||
| window.LZString = window.LZString || window.LZStringUnsafe; | ||||
| 
 | ||||
| // eslint-disable-next-line no-var
 | ||||
| var [chromeLocal, chromeSync] = [ | ||||
|   chrome.storage.local, | ||||
|   chrome.storage.sync, | ||||
| ].map(storage => { | ||||
|   const wrapper = { | ||||
|     get(options) { | ||||
|       return new Promise(resolve => { | ||||
|         storage.get(options, data => resolve(data)); | ||||
|       }); | ||||
|     }, | ||||
|     set(data) { | ||||
|       return new Promise(resolve => { | ||||
|         storage.set(data, () => resolve(data)); | ||||
|       }); | ||||
|     }, | ||||
|     remove(keyOrKeys) { | ||||
|       return new Promise(resolve => { | ||||
|         storage.remove(keyOrKeys, resolve); | ||||
|       }); | ||||
|     }, | ||||
|     getValue(key) { | ||||
|       return wrapper.get(key).then(data => data[key]); | ||||
|     }, | ||||
|     setValue(key, value) { | ||||
|       return wrapper.set({[key]: value}); | ||||
|     }, | ||||
|     getLZValue(key) { | ||||
|       return wrapper.getLZValues([key]).then(data => data[key]); | ||||
|     }, | ||||
|     getLZValues(keys) { | ||||
|       return wrapper.get(keys).then((data = {}) => { | ||||
|         for (const key of keys) { | ||||
|           const value = data[key]; | ||||
|           data[key] = value && tryJSONparse(LZString.decompressFromUTF16(value)); | ||||
|         } | ||||
|         return data; | ||||
|       }); | ||||
|     }, | ||||
|     setLZValue(key, value) { | ||||
|       return wrapper.set({[key]: LZString.compressToUTF16(JSON.stringify(value))}); | ||||
|     } | ||||
|   }; | ||||
|   return wrapper; | ||||
| }); | ||||
| 
 | ||||
| // eslint-disable-next-line no-var
 | ||||
| var dbExec = dbExecIndexedDB; | ||||
| dbExec.initialized = false; | ||||
|  | @ -247,6 +199,7 @@ function filterStyles({ | |||
|   matchUrl = null, | ||||
|   md5Url = null, | ||||
|   asHash = null, | ||||
|   omitCode, | ||||
|   strictRegexp = true, // used by the popup to detect bad regexps
 | ||||
| } = {}) { | ||||
|   enabled = enabled === null || typeof enabled === 'boolean' ? enabled : | ||||
|  | @ -274,24 +227,34 @@ function filterStyles({ | |||
| 
 | ||||
|   const cacheKey = [enabled, id, matchUrl, md5Url, asHash, strictRegexp].join('\t'); | ||||
|   const cached = cachedStyles.filters.get(cacheKey); | ||||
|   let styles; | ||||
|   if (cached) { | ||||
|     cached.hits++; | ||||
|     cached.lastHit = Date.now(); | ||||
|     return asHash | ||||
|     styles = asHash | ||||
|       ? Object.assign(blankHash, cached.styles) | ||||
|       : cached.styles; | ||||
|       : cached.styles.slice(); | ||||
|   } else { | ||||
|     styles = filterStylesInternal({ | ||||
|       enabled, | ||||
|       id, | ||||
|       matchUrl, | ||||
|       md5Url, | ||||
|       asHash, | ||||
|       strictRegexp, | ||||
|       blankHash, | ||||
|       cacheKey, | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   return filterStylesInternal({ | ||||
|     enabled, | ||||
|     id, | ||||
|     matchUrl, | ||||
|     md5Url, | ||||
|     asHash, | ||||
|     strictRegexp, | ||||
|     blankHash, | ||||
|     cacheKey, | ||||
|   }); | ||||
|   if (!omitCode) return styles; | ||||
|   if (!asHash) return styles.map(getStyleWithNoCode); | ||||
|   for (const id in styles) { | ||||
|     const style = styles[id]; | ||||
|     if (style && style.sections) { | ||||
|       styles[id] = getStyleWithNoCode(style); | ||||
|     } | ||||
|   } | ||||
|   return styles; | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
|  | @ -427,6 +390,7 @@ function saveStyle(style) { | |||
|         md5Url: null, | ||||
|         url: null, | ||||
|         originalMd5: null, | ||||
|         installDate: Date.now(), | ||||
|       }, style); | ||||
|       return write(style); | ||||
|     } | ||||
|  | @ -797,3 +761,47 @@ function handleCssTransitionBug({tabId, frameId, url, styles}) { | |||
|     return RX_CSS_TRANSITION_DETECTOR.test(code.substr(Math.max(0, pos - 10), 50)); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| /* | ||||
|   According to CSS4 @document specification the entire URL must match. | ||||
|   Stylish-for-Chrome implemented it incorrectly since the very beginning. | ||||
|   We'll detect styles that abuse the bug by finding the sections that | ||||
|   would have been applied by Stylish but not by us as we follow the spec. | ||||
|   Additionally we'll check for invalid regexps. | ||||
| */ | ||||
| function detectSloppyRegexps({matchUrl, ids}) { | ||||
|   const results = []; | ||||
|   for (const id of ids) { | ||||
|     const style = cachedStyles.byId.get(id); | ||||
|     if (!style) continue; | ||||
|     // make sure all regexps are compiled
 | ||||
|     const rxCache = cachedStyles.regexps; | ||||
|     let hasRegExp = false; | ||||
|     for (const section of style.sections) { | ||||
|       for (const regexp of section.regexps) { | ||||
|         hasRegExp = true; | ||||
|         for (let pass = 1; pass <= 2; pass++) { | ||||
|           const cacheKey = pass === 1 ? regexp : SLOPPY_REGEXP_PREFIX + regexp; | ||||
|           if (!rxCache.has(cacheKey)) { | ||||
|             // according to CSS4 @document specification the entire URL must match
 | ||||
|             const anchored = pass === 1 ? '^(?:' + regexp + ')$' : '^' + regexp + '$'; | ||||
|             // create in the bg context to avoid leaking of "dead objects"
 | ||||
|             const rx = tryRegExp(anchored); | ||||
|             rxCache.set(cacheKey, rx || false); | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     if (!hasRegExp) continue; | ||||
|     const applied = getApplicableSections({style, matchUrl}); | ||||
|     const wannabe = getApplicableSections({style, matchUrl, strictRegexp: false}); | ||||
|     results.push({ | ||||
|       id, | ||||
|       applied, | ||||
|       skipped: wannabe.length - applied.length, | ||||
|       hasInvalidRegexps: wannabe.some(({regexps}) => regexps.some(rx => !rxCache.has(rx))), | ||||
|     }); | ||||
|   } | ||||
|   return results; | ||||
| } | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| /* global getStyles */ | ||||
| /* global getStyles API_METHODS */ | ||||
| 'use strict'; | ||||
| 
 | ||||
| const styleViaAPI = !CHROME && (() => { | ||||
| API_METHODS.styleViaAPI = !CHROME && (() => { | ||||
|   const ACTIONS = { | ||||
|     styleApply, | ||||
|     styleDeleted, | ||||
|  |  | |||
|  | @ -1,47 +1,74 @@ | |||
| /* global getStyles, saveStyle, styleSectionsEqual, chromeLocal */ | ||||
| /* global calcStyleDigest */ | ||||
| /* global usercss semverCompare usercssHelper */ | ||||
| /* | ||||
| global getStyles saveStyle styleSectionsEqual | ||||
| global calcStyleDigest cachedStyles getStyleWithNoCode | ||||
| global usercss semverCompare | ||||
| global API_METHODS | ||||
| */ | ||||
| 'use strict'; | ||||
| 
 | ||||
| // eslint-disable-next-line no-var
 | ||||
| var updater = { | ||||
| var updater = (() => { | ||||
| 
 | ||||
|   COUNT: 'count', | ||||
|   UPDATED: 'updated', | ||||
|   SKIPPED: 'skipped', | ||||
|   DONE: 'done', | ||||
|   const STATES = { | ||||
|     UPDATED: 'updated', | ||||
|     SKIPPED: 'skipped', | ||||
| 
 | ||||
|   // details for SKIPPED status
 | ||||
|   EDITED: 'locally edited', | ||||
|   MAYBE_EDITED: 'may be locally edited', | ||||
|   SAME_MD5: 'up-to-date: MD5 is unchanged', | ||||
|   SAME_CODE: 'up-to-date: code sections are unchanged', | ||||
|   SAME_VERSION: 'up-to-date: version is unchanged', | ||||
|   ERROR_MD5: 'error: MD5 is invalid', | ||||
|   ERROR_JSON: 'error: JSON is invalid', | ||||
|   ERROR_VERSION: 'error: version is older than installed style', | ||||
|     // details for SKIPPED status
 | ||||
|     EDITED:        'locally edited', | ||||
|     MAYBE_EDITED:  'may be locally edited', | ||||
|     SAME_MD5:      'up-to-date: MD5 is unchanged', | ||||
|     SAME_CODE:     'up-to-date: code sections are unchanged', | ||||
|     SAME_VERSION:  'up-to-date: version is unchanged', | ||||
|     ERROR_MD5:     'error: MD5 is invalid', | ||||
|     ERROR_JSON:    'error: JSON is invalid', | ||||
|     ERROR_VERSION: 'error: version is older than installed style', | ||||
|   }; | ||||
| 
 | ||||
|   lastUpdateTime: parseInt(localStorage.lastUpdateTime) || Date.now(), | ||||
|   let lastUpdateTime = parseInt(localStorage.lastUpdateTime) || Date.now(); | ||||
|   let checkingAll = false; | ||||
|   let logQueue = []; | ||||
|   let logLastWriteTime = 0; | ||||
| 
 | ||||
|   checkAllStyles({observer = () => {}, save = true, ignoreDigest} = {}) { | ||||
|     updater.resetInterval(); | ||||
|     updater.checkAllStyles.running = true; | ||||
|   API_METHODS.updateCheckAll = checkAllStyles; | ||||
|   API_METHODS.updateCheck = checkStyle; | ||||
|   API_METHODS.getUpdaterStates = () => updater.STATES; | ||||
| 
 | ||||
|   prefs.subscribe(['updateInterval'], schedule); | ||||
|   schedule(); | ||||
| 
 | ||||
|   return {checkAllStyles, checkStyle, STATES}; | ||||
| 
 | ||||
|   function checkAllStyles({ | ||||
|     save = true, | ||||
|     ignoreDigest, | ||||
|     observe, | ||||
|   } = {}) { | ||||
|     resetInterval(); | ||||
|     checkingAll = true; | ||||
|     const port = observe && chrome.runtime.connect({name: 'updater'}); | ||||
|     return getStyles({}).then(styles => { | ||||
|       styles = styles.filter(style => style.updateUrl); | ||||
|       observer(updater.COUNT, styles.length); | ||||
|       updater.log(''); | ||||
|       updater.log(`${save ? 'Scheduled' : 'Manual'} update check for ${styles.length} styles`); | ||||
|       if (port) port.postMessage({count: styles.length}); | ||||
|       log(''); | ||||
|       log(`${save ? 'Scheduled' : 'Manual'} update check for ${styles.length} styles`); | ||||
|       return Promise.all( | ||||
|         styles.map(style => | ||||
|           updater.checkStyle({style, observer, save, ignoreDigest}))); | ||||
|           checkStyle({style, port, save, ignoreDigest}))); | ||||
|     }).then(() => { | ||||
|       observer(updater.DONE); | ||||
|       updater.log(''); | ||||
|       updater.checkAllStyles.running = false; | ||||
|       if (port) port.postMessage({done: true}); | ||||
|       if (port) port.disconnect(); | ||||
|       log(''); | ||||
|       checkingAll = false; | ||||
|     }); | ||||
|   }, | ||||
|   } | ||||
| 
 | ||||
|   checkStyle({style, observer = () => {}, save = true, ignoreDigest}) { | ||||
|   function checkStyle({ | ||||
|     id, | ||||
|     style = cachedStyles.byId.get(id), | ||||
|     port, | ||||
|     save = true, | ||||
|     ignoreDigest, | ||||
|   }) { | ||||
|     /* | ||||
|     Original style digests are calculated in these cases: | ||||
|     * style is installed or updated from server | ||||
|  | @ -65,29 +92,33 @@ var updater = { | |||
|       .catch(reportFailure); | ||||
| 
 | ||||
|     function reportSuccess(saved) { | ||||
|       observer(updater.UPDATED, saved); | ||||
|       updater.log(updater.UPDATED + ` #${style.id} ${style.name}`); | ||||
|       log(STATES.UPDATED + ` #${style.id} ${style.name}`); | ||||
|       const info = {updated: true, style: saved}; | ||||
|       if (port) port.postMessage(info); | ||||
|       return info; | ||||
|     } | ||||
| 
 | ||||
|     function reportFailure(err) { | ||||
|       observer(updater.SKIPPED, style, err); | ||||
|       err = err === 0 ? 'server unreachable' : err; | ||||
|       updater.log(updater.SKIPPED + ` (${err}) #${style.id} ${style.name}`); | ||||
|     function reportFailure(error) { | ||||
|       error = error === 0 ? 'server unreachable' : error; | ||||
|       log(STATES.SKIPPED + ` (${error}) #${style.id} ${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(updater.EDITED); | ||||
|         return Promise.reject(STATES.EDITED); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     function maybeUpdateUSO() { | ||||
|       return download(style.md5Url).then(md5 => { | ||||
|         if (!md5 || md5.length !== 32) { | ||||
|           return Promise.reject(updater.ERROR_MD5); | ||||
|           return Promise.reject(STATES.ERROR_MD5); | ||||
|         } | ||||
|         if (md5 === style.originalMd5 && style.originalDigest && !ignoreDigest) { | ||||
|           return Promise.reject(updater.SAME_MD5); | ||||
|           return Promise.reject(STATES.SAME_MD5); | ||||
|         } | ||||
|         return download(style.updateUrl) | ||||
|           .then(text => tryJSONparse(text)); | ||||
|  | @ -104,14 +135,14 @@ var updater = { | |||
|           case 0: | ||||
|             // re-install is invalid in a soft upgrade
 | ||||
|             if (!ignoreDigest) { | ||||
|               return Promise.reject(updater.SAME_VERSION); | ||||
|               return Promise.reject(STATES.SAME_VERSION); | ||||
|             } else if (text === style.sourceCode) { | ||||
|               return Promise.reject(updater.SAME_CODE); | ||||
|               return Promise.reject(STATES.SAME_CODE); | ||||
|             } | ||||
|             break; | ||||
|           case 1: | ||||
|             // downgrade is always invalid
 | ||||
|             return Promise.reject(updater.ERROR_VERSION); | ||||
|             return Promise.reject(STATES.ERROR_VERSION); | ||||
|         } | ||||
|         return usercss.buildCode(json); | ||||
|       }); | ||||
|  | @ -120,8 +151,9 @@ var updater = { | |||
|     function maybeSave(json = {}) { | ||||
|       // usercss is already validated while building
 | ||||
|       if (!json.usercssData && !styleJSONseemsValid(json)) { | ||||
|         return Promise.reject(updater.ERROR_JSON); | ||||
|         return Promise.reject(STATES.ERROR_JSON); | ||||
|       } | ||||
| 
 | ||||
|       json.id = style.id; | ||||
|       json.updateDate = Date.now(); | ||||
|       json.reason = 'update'; | ||||
|  | @ -139,15 +171,16 @@ var updater = { | |||
|       if (styleSectionsEqual(json, style)) { | ||||
|         // update digest even if save === false as there might be just a space added etc.
 | ||||
|         saveStyle(Object.assign(json, {reason: 'update-digest'})); | ||||
|         return Promise.reject(updater.SAME_CODE); | ||||
|       } else if (!style.originalDigest && !ignoreDigest) { | ||||
|         return Promise.reject(updater.MAYBE_EDITED); | ||||
|         return Promise.reject(STATES.SAME_CODE); | ||||
|       } | ||||
| 
 | ||||
|       return !save ? json : | ||||
|         json.usercssData | ||||
|           ? usercssHelper.save(json) | ||||
|           : saveStyle(json); | ||||
|       if (!style.originalDigest && !ignoreDigest) { | ||||
|         return Promise.reject(STATES.MAYBE_EDITED); | ||||
|       } | ||||
| 
 | ||||
|       return save ? | ||||
|         API_METHODS[json.usercssData ? 'saveUsercss' : 'saveStyle'](json) : | ||||
|         json; | ||||
|     } | ||||
| 
 | ||||
|     function styleJSONseemsValid(json) { | ||||
|  | @ -157,49 +190,47 @@ var updater = { | |||
|         && typeof json.sections.every === 'function' | ||||
|         && typeof json.sections[0].code === 'string'; | ||||
|     } | ||||
|   }, | ||||
|   } | ||||
| 
 | ||||
|   schedule() { | ||||
|   function schedule() { | ||||
|     const interval = prefs.get('updateInterval') * 60 * 60 * 1000; | ||||
|     if (interval) { | ||||
|       const elapsed = Math.max(0, Date.now() - updater.lastUpdateTime); | ||||
|       debounce(updater.checkAllStyles, Math.max(10e3, interval - elapsed)); | ||||
|       const elapsed = Math.max(0, Date.now() - lastUpdateTime); | ||||
|       debounce(checkAllStyles, Math.max(10e3, interval - elapsed)); | ||||
|     } else { | ||||
|       debounce.unregister(updater.checkAllStyles); | ||||
|       debounce.unregister(checkAllStyles); | ||||
|     } | ||||
|   }, | ||||
|   } | ||||
| 
 | ||||
|   resetInterval() { | ||||
|     localStorage.lastUpdateTime = updater.lastUpdateTime = Date.now(); | ||||
|     updater.schedule(); | ||||
|   }, | ||||
|   function resetInterval() { | ||||
|     localStorage.lastUpdateTime = lastUpdateTime = Date.now(); | ||||
|     schedule(); | ||||
|   } | ||||
| 
 | ||||
|   log: (() => { | ||||
|     let queue = []; | ||||
|     let lastWriteTime = 0; | ||||
|     return text => { | ||||
|       queue.push({text, time: new Date().toLocaleString()}); | ||||
|       debounce(flushQueue, text && updater.checkAllStyles.running ? 1000 : 0); | ||||
|     }; | ||||
|     function flushQueue() { | ||||
|       chromeLocal.getValue('updateLog').then((lines = []) => { | ||||
|         const time = Date.now() - lastWriteTime > 11e3 ? queue[0].time + ' ' : ''; | ||||
|         if (!queue[0].text) { | ||||
|           queue.shift(); | ||||
|           if (lines[lines.length - 1]) { | ||||
|             lines.push(''); | ||||
|           } | ||||
|         } | ||||
|         lines.splice(0, lines.length - 1000); | ||||
|         lines.push(time + queue[0].text); | ||||
|         lines.push(...queue.slice(1).map(item => item.text)); | ||||
|         chromeLocal.setValue('updateLog', lines); | ||||
|         lastWriteTime = Date.now(); | ||||
|         queue = []; | ||||
|       }); | ||||
|   function log(text) { | ||||
|     logQueue.push({text, time: new Date().toLocaleString()}); | ||||
|     debounce(flushQueue, text && checkingAll ? 1000 : 0); | ||||
|   } | ||||
| 
 | ||||
|   function flushQueue(stored) { | ||||
|     if (!stored) { | ||||
|       chrome.storage.local.get('updateLog', flushQueue); | ||||
|       return; | ||||
|     } | ||||
|   })(), | ||||
| }; | ||||
|     const lines = stored.lines || []; | ||||
|     const time = Date.now() - logLastWriteTime > 11e3 ? | ||||
|       logQueue[0].time + ' ' : | ||||
|       ''; | ||||
|     if (!logQueue[0].text) { | ||||
|       logQueue.shift(); | ||||
|       if (lines[lines.length - 1]) lines.push(''); | ||||
|     } | ||||
|     lines.splice(0, lines.length - 1000); | ||||
|     lines.push(time + (logQueue[0] && logQueue[0].text || '')); | ||||
|     lines.push(...logQueue.slice(1).map(item => item.text)); | ||||
| 
 | ||||
| updater.schedule(); | ||||
| prefs.subscribe(['updateInterval'], updater.schedule); | ||||
|     chrome.storage.local.set({updateLog: lines}); | ||||
|     logLastWriteTime = Date.now(); | ||||
|     logQueue = []; | ||||
|   } | ||||
| })(); | ||||
|  |  | |||
|  | @ -1,8 +1,11 @@ | |||
| /* global usercss saveStyle getStyles chromeLocal */ | ||||
| /* global API_METHODS usercss saveStyle getStyles chromeLocal cachedStyles */ | ||||
| 'use strict'; | ||||
| 
 | ||||
| // eslint-disable-next-line no-var
 | ||||
| var usercssHelper = (() => { | ||||
| (() => { | ||||
| 
 | ||||
|   API_METHODS.saveUsercss = save; | ||||
|   API_METHODS.buildUsercss = build; | ||||
|   API_METHODS.installUsercss = install; | ||||
| 
 | ||||
|   const TEMP_CODE_PREFIX = 'tempUsercssCode'; | ||||
|   const TEMP_CODE_CLEANUP_DELAY = 60e3; | ||||
|  | @ -48,31 +51,25 @@ var usercssHelper = (() => { | |||
|     return usercss.buildCode(style); | ||||
|   } | ||||
| 
 | ||||
|   function wrapReject(pending) { | ||||
|     return pending | ||||
|       .catch(err => new Error(Array.isArray(err) ? err.join('\n') : err.message || String(err))); | ||||
|   } | ||||
| 
 | ||||
|   // Parse the source and find the duplication
 | ||||
|   function build({sourceCode, checkDup = false}, noReject) { | ||||
|     const pending = buildMeta({sourceCode}) | ||||
|   function build({sourceCode, checkDup = false}) { | ||||
|     return buildMeta({sourceCode}) | ||||
|       .then(style => Promise.all([ | ||||
|         buildCode(style), | ||||
|         checkDup && findDup(style) | ||||
|       ])) | ||||
|       .then(([style, dup]) => ({style, dup})); | ||||
| 
 | ||||
|     return noReject ? wrapReject(pending) : pending; | ||||
|   } | ||||
| 
 | ||||
|   function save(style, noReject) { | ||||
|     const pending = buildMeta(style) | ||||
|   function save(style) { | ||||
|     if (!style.sourceCode) { | ||||
|       style.sourceCode = cachedStyles.byId.get(style.id).sourceCode; | ||||
|     } | ||||
|     return buildMeta(style) | ||||
|       .then(assignVars) | ||||
|       .then(buildCode) | ||||
|       .then(saveStyle); | ||||
| 
 | ||||
|     return noReject ? wrapReject(pending) : pending; | ||||
| 
 | ||||
|     function assignVars(style) { | ||||
|       if (style.reason === 'config' && style.id) { | ||||
|         return style; | ||||
|  | @ -105,11 +102,12 @@ var usercssHelper = (() => { | |||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   function openInstallPage(tab, {url = tab.url, direct, downloaded} = {}) { | ||||
|   function install({url, direct, downloaded}, {tab}) { | ||||
|     url = url || tab.url; | ||||
|     if (direct && !downloaded) { | ||||
|       prefetchCodeForInstallation(tab.id, url); | ||||
|     } | ||||
|     return wrapReject(openURL({ | ||||
|     return openURL({ | ||||
|       url: '/install-usercss.html' + | ||||
|         '?updateUrl=' + encodeURIComponent(url) + | ||||
|         '&tabId=' + tab.id + | ||||
|  | @ -117,7 +115,7 @@ var usercssHelper = (() => { | |||
|       index: tab.index + 1, | ||||
|       openerTabId: tab.id, | ||||
|       currentWindow: null, | ||||
|     })); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   function prefetchCodeForInstallation(tabId, url) { | ||||
|  | @ -131,6 +129,4 @@ var usercssHelper = (() => { | |||
|       setTimeout(() => chromeLocal.remove(key), TEMP_CODE_CLEANUP_DELAY); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   return {build, save, findDup, openInstallPage}; | ||||
| })(); | ||||
|  |  | |||
|  | @ -48,8 +48,8 @@ | |||
|       asHash: true, | ||||
|     }, options); | ||||
|     // On own pages we request the styles directly to minimize delay and flicker
 | ||||
|     if (typeof getStylesSafe === 'function') { | ||||
|       getStylesSafe(request).then(callback); | ||||
|     if (typeof API === 'function') { | ||||
|       API.getStyles(request).then(callback); | ||||
|     } else { | ||||
|       chrome.runtime.sendMessage(request, callback); | ||||
|     } | ||||
|  |  | |||
|  | @ -97,7 +97,7 @@ function initUsercssInstall() { | |||
|     }); | ||||
|   }); | ||||
|   chrome.runtime.sendMessage({ | ||||
|     method: 'openUsercssInstallPage', | ||||
|     method: 'installUsercss', | ||||
|     url: location.href, | ||||
|   }, r => r && r.__ERROR__ && alert(r.__ERROR__)); | ||||
| } | ||||
|  |  | |||
|  | @ -198,7 +198,14 @@ | |||
|       if (url.startsWith('#')) { | ||||
|         resolve(document.getElementById(url.slice(1)).textContent); | ||||
|       } else { | ||||
|         chrome.runtime.sendMessage({method: 'download', url}, resolve); | ||||
|         chrome.runtime.sendMessage({method: 'download', url}, result => { | ||||
|           const error = result && result.__ERROR__; | ||||
|           if (error) { | ||||
|             alert('Error' + (error ? '\n' + error : '')); | ||||
|           } else { | ||||
|             resolve(result); | ||||
|           } | ||||
|         }); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|  |  | |||
|  | @ -23,6 +23,7 @@ | |||
|     <script src="js/localization.js"></script> | ||||
|     <script src="js/script-loader.js"></script> | ||||
|     <script src="js/moz-parser.js"></script> | ||||
|     <script src="js/storage-util.js"></script> | ||||
|     <script src="content/apply.js"></script> | ||||
|     <script src="edit/lint.js"></script> | ||||
|     <script src="edit/util.js"></script> | ||||
|  |  | |||
							
								
								
									
										25
									
								
								edit/edit.js
									
									
									
									
									
								
							
							
						
						
									
										25
									
								
								edit/edit.js
									
									
									
									
									
								
							|  | @ -44,7 +44,7 @@ Promise.all([ | |||
|   if (usercss) { | ||||
|     editor = createSourceEditor(style); | ||||
|   } else { | ||||
|     initWithSectionStyle({style}); | ||||
|     initWithSectionStyle(style); | ||||
|     document.addEventListener('wheel', scrollEntirePageOnCtrlShift); | ||||
|   } | ||||
| }); | ||||
|  | @ -155,14 +155,17 @@ function onRuntimeMessage(request) { | |||
|           request.reason !== 'editSave' && | ||||
|           request.reason !== 'config') { | ||||
|         // code-less style from notifyAllTabs
 | ||||
|         if ((request.style.sections[0] || {}).code === null) { | ||||
|           request.style = BG.cachedStyles.byId.get(request.style.id); | ||||
|         } | ||||
|         if (isUsercss(request.style)) { | ||||
|           editor.replaceStyle(request.style, request.codeIsUpdated); | ||||
|         } else { | ||||
|           initWithSectionStyle(request); | ||||
|         } | ||||
|         const {sections, id} = request.style; | ||||
|         ((sections[0] || {}).code === null | ||||
|           ? API.getStyles({id}) | ||||
|           : Promise.resolve([request.style]) | ||||
|         ).then(([style]) => { | ||||
|           if (isUsercss(style)) { | ||||
|             editor.replaceStyle(style, request.codeIsUpdated); | ||||
|           } else { | ||||
|             initWithSectionStyle(style, request.codeIsUpdated); | ||||
|           } | ||||
|         }); | ||||
|       } | ||||
|       break; | ||||
|     case 'styleDeleted': | ||||
|  | @ -228,7 +231,7 @@ function initStyleData() { | |||
|       ) | ||||
|     ], | ||||
|   }); | ||||
|   return getStylesSafe({id: id || -1}) | ||||
|   return API.getStyles({id: id || -1}) | ||||
|     .then(([style = createEmptyStyle()]) => { | ||||
|       styleId = style.id; | ||||
|       if (styleId) sessionStorage.justEditedStyleId = styleId; | ||||
|  | @ -344,7 +347,7 @@ function save() { | |||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   saveStyleSafe({ | ||||
|   API.saveStyle({ | ||||
|     id: styleId, | ||||
|     name: $('#name').value.trim(), | ||||
|     enabled: $('#enabled').checked, | ||||
|  |  | |||
							
								
								
									
										10
									
								
								edit/lint.js
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								edit/lint.js
									
									
									
									
									
								
							|  | @ -121,12 +121,12 @@ var linterConfig = { | |||
|     config = this.fallbackToDefaults(config); | ||||
|     const linter = linterConfig.getName(); | ||||
|     this[linter] = config; | ||||
|     BG.chromeSync.setLZValue(this.storageName[linter], config); | ||||
|     chromeSync.setLZValue(this.storageName[linter], config); | ||||
|     return config; | ||||
|   }, | ||||
| 
 | ||||
|   loadAll() { | ||||
|     return BG.chromeSync.getLZValues([ | ||||
|     return chromeSync.getLZValues([ | ||||
|       'editorCSSLintConfig', | ||||
|       'editorStylelintConfig', | ||||
|     ]).then(data => { | ||||
|  | @ -167,10 +167,8 @@ var linterConfig = { | |||
|   }, | ||||
| 
 | ||||
|   init() { | ||||
|     if (!linterConfig.init.pending) { | ||||
|       linterConfig.init.pending = linterConfig.loadAll(); | ||||
|     } | ||||
|     return linterConfig.init.pending; | ||||
|     if (!this.init.pending) this.init.pending = this.loadAll(); | ||||
|     return this.init.pending; | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ global showAppliesToHelp beautify regExpTester setGlobalProgress setCleanSection | |||
| */ | ||||
| 'use strict'; | ||||
| 
 | ||||
| function initWithSectionStyle({style, codeIsUpdated}) { | ||||
| function initWithSectionStyle(style, codeIsUpdated) { | ||||
|   $('#name').value = style.name || ''; | ||||
|   $('#enabled').checked = style.enabled !== false; | ||||
|   $('#url').href = style.url || ''; | ||||
|  |  | |||
|  | @ -106,7 +106,7 @@ function createSourceEditor(style) { | |||
|     `.replace(/^\s+/gm, '');
 | ||||
|     dirty.clear('sourceGeneration'); | ||||
|     style.sourceCode = ''; | ||||
|     BG.chromeSync.getLZValue('usercssTemplate').then(code => { | ||||
|     chromeSync.getLZValue('usercssTemplate').then(code => { | ||||
|       style.sourceCode = code || DEFAULT_CODE; | ||||
|       cm.startOperation(); | ||||
|       cm.setValue(style.sourceCode); | ||||
|  | @ -216,8 +216,8 @@ function createSourceEditor(style) { | |||
|       return; | ||||
|     } | ||||
|     const code = cm.getValue(); | ||||
|     return onBackgroundReady() | ||||
|       .then(() => BG.usercssHelper.save({ | ||||
|     return ( | ||||
|       API.saveUsercss({ | ||||
|         reason: 'editSave', | ||||
|         id: style.id, | ||||
|         enabled: style.enabled, | ||||
|  | @ -228,8 +228,8 @@ function createSourceEditor(style) { | |||
|       .catch(err => { | ||||
|         if (err.message === t('styleMissingMeta', 'name')) { | ||||
|           messageBox.confirm(t('usercssReplaceTemplateConfirmation')).then(ok => ok && | ||||
|             BG.chromeSync.setLZValue('usercssTemplate', code) | ||||
|               .then(() => BG.chromeSync.getLZValue('usercssTemplate')) | ||||
|             chromeSync.setLZValue('usercssTemplate', code) | ||||
|               .then(() => chromeSync.getLZValue('usercssTemplate')) | ||||
|               .then(saved => saved !== code && messageBox.alert(t('syncStorageErrorSaving')))); | ||||
|           return; | ||||
|         } | ||||
|  |  | |||
|  | @ -200,7 +200,7 @@ | |||
|     if (!liveReload && !prefs.get('openEditInWindow')) { | ||||
|       chrome.tabs.update({url: '/edit.html?id=' + style.id}); | ||||
|     } else { | ||||
|       BG.openEditor(style.id); | ||||
|       API.openEditor({id: style.id}); | ||||
|       if (!liveReload) { | ||||
|         closeCurrentTab(); | ||||
|       } | ||||
|  | @ -212,8 +212,8 @@ | |||
|   function initSourceCode(sourceCode) { | ||||
|     cm.setValue(sourceCode); | ||||
|     cm.refresh(); | ||||
|     BG.usercssHelper.build(BG.deepCopy({sourceCode, checkDup: true})) | ||||
|       .then(r => init(deepCopy(r))) | ||||
|     API.buildUsercss({sourceCode, checkDup: true}) | ||||
|       .then(r => init(r instanceof Object ? r : deepCopy(r))) | ||||
|       .catch(err => { | ||||
|         $('.header').classList.add('meta-init-error'); | ||||
|         showError(err); | ||||
|  | @ -222,7 +222,7 @@ | |||
| 
 | ||||
|   function buildWarning(err) { | ||||
|     const contents = Array.isArray(err) ? | ||||
|       $create('pre', err.join('\n')) : | ||||
|       [$create('pre', err.join('\n'))] : | ||||
|       [err && err.message || err || 'Unknown error']; | ||||
|     if (Number.isInteger(err.index)) { | ||||
|       const pos = cm.posFromIndex(err.index); | ||||
|  | @ -283,8 +283,8 @@ | |||
|           data.version, | ||||
|         ])) | ||||
|       ).then(ok => ok && | ||||
|         BG.usercssHelper.save(BG.deepCopy(Object.assign(style, dup && {reason: 'update'}))) | ||||
|           .then(r => install(deepCopy(r))) | ||||
|         API.saveUsercss(Object.assign(style, dup && {reason: 'update'})) | ||||
|           .then(r => install(r instanceof Object ? r : deepCopy(r))) | ||||
|           .catch(err => messageBox.alert(t('styleInstallFailed', err))) | ||||
|       ); | ||||
|     }; | ||||
|  |  | |||
|  | @ -64,7 +64,8 @@ onDOMready().then(() => { | |||
| if (!chrome.app && chrome.windows) { | ||||
|   // die if unable to access BG directly
 | ||||
|   chrome.windows.getCurrent(wnd => { | ||||
|     if (!BG && wnd.incognito) { | ||||
|     if (!BG && wnd.incognito && | ||||
|         !location.pathname.includes('popup.html')) { | ||||
|       // private windows can't get bg page
 | ||||
|       location.href = '/msgbox/dysfunctional.html'; | ||||
|       throw 0; | ||||
|  |  | |||
							
								
								
									
										181
									
								
								js/messaging.js
									
									
									
									
									
								
							
							
						
						
									
										181
									
								
								js/messaging.js
									
									
									
									
									
								
							|  | @ -1,14 +1,18 @@ | |||
| /* global BG: true, onRuntimeMessage, applyOnMessage, handleUpdate, handleDelete */ | ||||
| /* global FIREFOX: true */ | ||||
| /* | ||||
| global BG: true | ||||
| global FIREFOX: true | ||||
| global onRuntimeMessage applyOnMessage | ||||
| */ | ||||
| 'use strict'; | ||||
| 
 | ||||
| // keep message channel open for sendResponse in chrome.runtime.onMessage listener
 | ||||
| const KEEP_CHANNEL_OPEN = true; | ||||
| 
 | ||||
| const CHROME = Boolean(chrome.app) && parseInt(navigator.userAgent.match(/Chrom\w+\/(?:\d+\.){2}(\d+)|$/)[1]); | ||||
| const OPERA = CHROME && parseFloat(navigator.userAgent.match(/\bOPR\/(\d+\.\d+)|$/)[1]); | ||||
| const OPERA = Boolean(chrome.app) && parseFloat(navigator.userAgent.match(/\bOPR\/(\d+\.\d+)|$/)[1]); | ||||
| const VIVALDI = Boolean(chrome.app) && navigator.userAgent.includes('Vivaldi'); | ||||
| const ANDROID = !chrome.windows; | ||||
| let FIREFOX = !CHROME && parseFloat(navigator.userAgent.match(/\bFirefox\/(\d+\.\d+)|$/)[1]); | ||||
| let FIREFOX = !chrome.app && parseFloat(navigator.userAgent.match(/\bFirefox\/(\d+\.\d+)|$/)[1]); | ||||
| 
 | ||||
| if (!CHROME && !chrome.browserAction.openPopup) { | ||||
|   // in FF pre-57 legacy addons can override useragent so we assume the worst
 | ||||
|  | @ -65,13 +69,14 @@ if (!BG || BG !== window) { | |||
|     document.documentElement.classList.add('firefox'); | ||||
|   } else if (OPERA) { | ||||
|     document.documentElement.classList.add('opera'); | ||||
|   } else if (chrome.app && navigator.userAgent.includes('Vivaldi')) { | ||||
|     document.documentElement.classList.add('vivaldi'); | ||||
|   } else { | ||||
|     if (VIVALDI) document.documentElement.classList.add('vivaldi'); | ||||
|   } | ||||
|   // TODO: remove once our manifest's minimum_chrome_version is 50+
 | ||||
|   // Chrome 49 doesn't report own extension pages in webNavigation apparently
 | ||||
|   if (CHROME && CHROME < 2661) { | ||||
|     getActiveTab().then(BG.updateIcon); | ||||
|     getActiveTab().then(tab => | ||||
|       window.API.updateIcon({tab})); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
|  | @ -82,6 +87,60 @@ if (FIREFOX_NO_DOM_STORAGE) { | |||
|   Object.defineProperty(window, 'sessionStorage', {value: {}}); | ||||
| } | ||||
| 
 | ||||
| // eslint-disable-next-line no-var
 | ||||
| var API = (() => { | ||||
|   return new Proxy(() => {}, { | ||||
|     get: (target, name) => | ||||
|       name === 'remoteCall' ? | ||||
|         remoteCall : | ||||
|         arg => invokeBG(name, arg), | ||||
|   }); | ||||
| 
 | ||||
|   function remoteCall(name, arg, remoteWindow) { | ||||
|     let thing = window[name] || window.API_METHODS[name]; | ||||
|     if (typeof thing === 'function') { | ||||
|       thing = thing(arg); | ||||
|     } | ||||
|     if (!thing || typeof thing !== 'object') { | ||||
|       return thing; | ||||
|     } else if (thing instanceof Promise) { | ||||
|       return thing.then(product => remoteWindow.deepCopy(product)); | ||||
|     } else { | ||||
|       return remoteWindow.deepCopy(thing); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   function invokeBG(name, arg = {}) { | ||||
|     if (BG && (name in BG || name in BG.API_METHODS)) { | ||||
|       const call = BG !== window ? | ||||
|         BG.API.remoteCall(name, BG.deepCopy(arg), window) : | ||||
|         remoteCall(name, arg, BG); | ||||
|       return Promise.resolve(call); | ||||
|     } | ||||
|     if (BG && BG.getStyles) { | ||||
|       throw new Error('Bad API method', name, arg); | ||||
|     } | ||||
|     if (FIREFOX) { | ||||
|       arg.method = name; | ||||
|       return sendMessage(arg); | ||||
|     } | ||||
|     return onBackgroundReady().then(() => invokeBG(name, arg)); | ||||
|   } | ||||
| 
 | ||||
|   function onBackgroundReady() { | ||||
|     return BG && BG.getStyles ? Promise.resolve() : new Promise(function ping(resolve) { | ||||
|       sendMessage({method: 'healthCheck'}, health => { | ||||
|         if (health !== undefined) { | ||||
|           BG = chrome.extension.getBackgroundPage(); | ||||
|           resolve(); | ||||
|         } else { | ||||
|           setTimeout(ping, 0, resolve); | ||||
|         } | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
| })(); | ||||
| 
 | ||||
| 
 | ||||
| function notifyAllTabs(msg) { | ||||
|   const originalMessage = msg; | ||||
|  | @ -99,6 +158,12 @@ function notifyAllTabs(msg) { | |||
|   const affectsIcon = affectsAll || msg.affects.icon; | ||||
|   const affectsPopup = affectsAll || msg.affects.popup; | ||||
|   const affectsSelf = affectsPopup || msg.prefs; | ||||
|   // notify background page and all open popups
 | ||||
|   if (affectsSelf) { | ||||
|     msg.tabId = undefined; | ||||
|     sendMessage(msg, ignoreChromeError); | ||||
|   } | ||||
|   // notify tabs
 | ||||
|   if (affectsTabs || affectsIcon) { | ||||
|     const notifyTab = tab => { | ||||
|       // own pages will be notified via runtime.sendMessage later
 | ||||
|  | @ -109,8 +174,9 @@ function notifyAllTabs(msg) { | |||
|         msg.tabId = tab.id; | ||||
|         sendMessage(msg, ignoreChromeError); | ||||
|       } | ||||
|       if (affectsIcon && BG) { | ||||
|         BG.updateIcon(tab); | ||||
|       if (affectsIcon) { | ||||
|         // eslint-disable-next-line no-use-before-define
 | ||||
|         debounce(API.updateIcon, 0, {tab}); | ||||
|       } | ||||
|     }; | ||||
|     // list all tabs including chrome-extension:// which can be ours
 | ||||
|  | @ -132,11 +198,6 @@ function notifyAllTabs(msg) { | |||
|   if (typeof applyOnMessage !== 'undefined') { | ||||
|     applyOnMessage(originalMessage); | ||||
|   } | ||||
|   // notify background page and all open popups
 | ||||
|   if (affectsSelf) { | ||||
|     msg.tabId = undefined; | ||||
|     sendMessage(msg, ignoreChromeError); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
|  | @ -294,10 +355,9 @@ function ignoreChromeError() { | |||
| 
 | ||||
| 
 | ||||
| function getStyleWithNoCode(style) { | ||||
|   const stripped = Object.assign({}, style, {sections: []}); | ||||
|   for (const section of style.sections) { | ||||
|     stripped.sections.push(Object.assign({}, section, {code: null})); | ||||
|   } | ||||
|   const stripped = deepCopy(style); | ||||
|   for (const section of stripped.sections) section.code = null; | ||||
|   stripped.sourceCode = null; | ||||
|   return stripped; | ||||
| } | ||||
| 
 | ||||
|  | @ -343,31 +403,23 @@ const debounce = Object.assign((fn, delay, ...args) => { | |||
| 
 | ||||
| 
 | ||||
| function deepCopy(obj) { | ||||
|   return obj !== null && obj !== undefined && typeof obj === 'object' | ||||
|     ? deepMerge(typeof obj.slice === 'function' ? [] : {}, obj) | ||||
|     : obj; | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| function deepMerge(target, ...args) { | ||||
|   const isArray = typeof target.slice === 'function'; | ||||
|   for (const obj of args) { | ||||
|     if (isArray && obj !== null && obj !== undefined) { | ||||
|       for (const element of obj) { | ||||
|         target.push(deepCopy(element)); | ||||
|       } | ||||
|       continue; | ||||
|     } | ||||
|     for (const k in obj) { | ||||
|       const value = obj[k]; | ||||
|       if (k in target && typeof value === 'object' && value !== null) { | ||||
|         deepMerge(target[k], value); | ||||
|       } else { | ||||
|         target[k] = deepCopy(value); | ||||
|       } | ||||
|   if (!obj || typeof obj !== 'object') return obj; | ||||
|   // N.B. a copy should be an explicitly  literal
 | ||||
|   if (Array.isArray(obj)) { | ||||
|     const copy = []; | ||||
|     for (const v of obj) { | ||||
|       copy.push(!v || typeof v !== 'object' ? v : deepCopy(v)); | ||||
|     } | ||||
|     return copy; | ||||
|   } | ||||
|   return target; | ||||
|   const copy = {}; | ||||
|   const hasOwnProperty = Object.prototype.hasOwnProperty; | ||||
|   for (const k in obj) { | ||||
|     if (!hasOwnProperty.call(obj, k)) continue; | ||||
|     const v = obj[k]; | ||||
|     copy[k] = !v || typeof v !== 'object' ? v : deepCopy(v); | ||||
|   } | ||||
|   return copy; | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
|  | @ -390,51 +442,6 @@ function sessionStorageHash(name) { | |||
| } | ||||
| 
 | ||||
| 
 | ||||
| function onBackgroundReady() { | ||||
|   return BG && BG.getStyles ? Promise.resolve() : new Promise(function ping(resolve) { | ||||
|     sendMessage({method: 'healthCheck'}, health => { | ||||
|       if (health !== undefined) { | ||||
|         BG = chrome.extension.getBackgroundPage(); | ||||
|         resolve(); | ||||
|       } else { | ||||
|         setTimeout(ping, 0, resolve); | ||||
|       } | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| // in case Chrome haven't yet loaded the bg page and displays our page like edit/manage
 | ||||
| function getStylesSafe(options) { | ||||
|   return onBackgroundReady() | ||||
|     .then(() => BG.getStyles(options)); | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| function saveStyleSafe(style) { | ||||
|   return onBackgroundReady() | ||||
|     .then(() => BG.saveStyle(BG.deepCopy(style))) | ||||
|     .then(savedStyle => { | ||||
|       if (style.notify === false) { | ||||
|         handleUpdate(savedStyle, style); | ||||
|       } | ||||
|       return savedStyle; | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| function deleteStyleSafe({id, notify = true} = {}) { | ||||
|   return onBackgroundReady() | ||||
|     .then(() => BG.deleteStyle({id, notify})) | ||||
|     .then(() => { | ||||
|       if (!notify) { | ||||
|         handleDelete(id); | ||||
|       } | ||||
|       return id; | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| function download(url, { | ||||
|   method = url.includes('?') ? 'POST' : 'GET', | ||||
|   body = url.includes('?') ? url.slice(url.indexOf('?')) : null, | ||||
|  | @ -489,7 +496,7 @@ function invokeOrPostpone(isInvoke, fn, ...args) { | |||
| } | ||||
| 
 | ||||
| 
 | ||||
| function openEditor(id) { | ||||
| function openEditor({id}) { | ||||
|   let url = '/edit.html'; | ||||
|   if (id) { | ||||
|     url += `?id=${id}`; | ||||
|  |  | |||
							
								
								
									
										133
									
								
								js/prefs.js
									
									
									
									
									
								
							
							
						
						
									
										133
									
								
								js/prefs.js
									
									
									
									
									
								
							|  | @ -148,17 +148,13 @@ var prefs = new function Prefs() { | |||
|       values[key] = value; | ||||
|       defineReadonlyProperty(this.readOnlyValues, key, value); | ||||
|       const hasChanged = !equal(value, oldValue); | ||||
|       if (!fromBroadcast) { | ||||
|         if (BG && BG !== window) { | ||||
|           BG.prefs.set(key, BG.deepCopy(value), {broadcast, sync}); | ||||
|         } else { | ||||
|           localStorage[key] = typeof defaults[key] === 'object' | ||||
|             ? JSON.stringify(value) | ||||
|             : value; | ||||
|           if (broadcast && hasChanged) { | ||||
|             this.broadcast(key, value, {sync}); | ||||
|           } | ||||
|         } | ||||
|       if (!fromBroadcast || FIREFOX_NO_DOM_STORAGE) { | ||||
|         localStorage[key] = typeof defaults[key] === 'object' | ||||
|           ? JSON.stringify(value) | ||||
|           : value; | ||||
|       } | ||||
|       if (!fromBroadcast && broadcast && hasChanged) { | ||||
|         this.broadcast(key, value, {sync}); | ||||
|       } | ||||
|       if (hasChanged) { | ||||
|         const specific = onChange.specific.get(key); | ||||
|  | @ -175,8 +171,6 @@ var prefs = new function Prefs() { | |||
|       } | ||||
|     }, | ||||
| 
 | ||||
|     remove: key => this.set(key, undefined), | ||||
| 
 | ||||
|     reset: key => this.set(key, deepCopy(defaults[key])), | ||||
| 
 | ||||
|     broadcast(key, value, {sync = true} = {}) { | ||||
|  | @ -226,62 +220,63 @@ var prefs = new function Prefs() { | |||
|     }, | ||||
|   }); | ||||
| 
 | ||||
|   // Unlike sync, HTML5 localStorage is ready at browser startup
 | ||||
|   // so we'll mirror the prefs to avoid using the wrong defaults
 | ||||
|   // during the startup phase
 | ||||
|   for (const key in defaults) { | ||||
|     const defaultValue = defaults[key]; | ||||
|     let value = localStorage[key]; | ||||
|     if (typeof value === 'string') { | ||||
|       switch (typeof defaultValue) { | ||||
|         case 'boolean': | ||||
|           value = value.toLowerCase() === 'true'; | ||||
|           break; | ||||
|         case 'number': | ||||
|           value |= 0; | ||||
|           break; | ||||
|         case 'object': | ||||
|           value = tryJSONparse(value) || defaultValue; | ||||
|           break; | ||||
|       } | ||||
|     } else if (FIREFOX_NO_DOM_STORAGE && BG) { | ||||
|       value = BG.localStorage[key]; | ||||
|       value = value === undefined ? defaultValue : value; | ||||
|     } else { | ||||
|       value = defaultValue; | ||||
|     } | ||||
|     if (BG === window) { | ||||
|       // when in bg page, .set() will write to localStorage
 | ||||
|       this.set(key, value, {broadcast: false, sync: false}); | ||||
|     } else { | ||||
|       values[key] = value; | ||||
|       defineReadonlyProperty(this.readOnlyValues, key, value); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   if (!BG || BG === window) { | ||||
|     affectsIcon.forEach(key => this.broadcast(key, values[key], {sync: false})); | ||||
| 
 | ||||
|     const importFromSync = (synced = {}) => { | ||||
|   { | ||||
|     const importFromBG = () => | ||||
|       API.getPrefs().then(prefs => { | ||||
|         const props = {}; | ||||
|         for (const id in prefs) { | ||||
|           const value = prefs[id]; | ||||
|           values[id] = value; | ||||
|           props[id] = {value: deepCopy(value)}; | ||||
|         } | ||||
|         Object.defineProperties(this.readOnlyValues, props); | ||||
|       }); | ||||
|     // Unlike chrome.storage or messaging, HTML5 localStorage is synchronous and always ready,
 | ||||
|     // so we'll mirror the prefs to avoid using the wrong defaults during the startup phase
 | ||||
|     const importFromLocalStorage = () => { | ||||
|       for (const key in defaults) { | ||||
|         if (key in synced) { | ||||
|           this.set(key, synced[key], {sync: false}); | ||||
|         } | ||||
|       } | ||||
|     }; | ||||
| 
 | ||||
|     getSync().get('settings', ({settings} = {}) => importFromSync(settings)); | ||||
| 
 | ||||
|     chrome.storage.onChanged.addListener((changes, area) => { | ||||
|       if (area === 'sync' && 'settings' in changes) { | ||||
|         const synced = changes.settings.newValue; | ||||
|         if (synced) { | ||||
|           importFromSync(synced); | ||||
|         const defaultValue = defaults[key]; | ||||
|         let value = localStorage[key]; | ||||
|         if (typeof value === 'string') { | ||||
|           switch (typeof defaultValue) { | ||||
|             case 'boolean': | ||||
|               value = value.toLowerCase() === 'true'; | ||||
|               break; | ||||
|             case 'number': | ||||
|               value |= 0; | ||||
|               break; | ||||
|             case 'object': | ||||
|               value = tryJSONparse(value) || defaultValue; | ||||
|               break; | ||||
|           } | ||||
|         } else if (FIREFOX_NO_DOM_STORAGE && BG) { | ||||
|           value = BG.localStorage[key]; | ||||
|           value = value === undefined ? defaultValue : value; | ||||
|           localStorage[key] = value; | ||||
|         } else { | ||||
|           // user manually deleted our settings, we'll recreate them
 | ||||
|           getSync().set({'settings': values}); | ||||
|           value = defaultValue; | ||||
|         } | ||||
|         if (BG === window) { | ||||
|           // when in bg page, .set() will write to localStorage
 | ||||
|           this.set(key, value, {broadcast: false, sync: false}); | ||||
|         } else { | ||||
|           values[key] = value; | ||||
|           defineReadonlyProperty(this.readOnlyValues, key, value); | ||||
|         } | ||||
|       } | ||||
|       return Promise.resolve(); | ||||
|     }; | ||||
|     (FIREFOX_NO_DOM_STORAGE && !BG ? importFromBG() : importFromLocalStorage()).then(() => { | ||||
|       if (BG && BG !== window) return; | ||||
|       if (BG === window) { | ||||
|         affectsIcon.forEach(key => this.broadcast(key, values[key], {sync: false})); | ||||
|         getSync().get('settings', data => importFromSync.call(this, data.settings)); | ||||
|       } | ||||
|       chrome.storage.onChanged.addListener((changes, area) => { | ||||
|         if (area === 'sync' && 'settings' in changes) { | ||||
|           importFromSync.call(this, changes.settings.newValue); | ||||
|         } | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|  | @ -350,6 +345,14 @@ var prefs = new function Prefs() { | |||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   function importFromSync(synced = {}) { | ||||
|     for (const key in defaults) { | ||||
|       if (key in synced) { | ||||
|         this.set(key, synced[key], {sync: false}); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   function defineReadonlyProperty(obj, key, value) { | ||||
|     const copy = deepCopy(value); | ||||
|     if (typeof copy === 'object') { | ||||
|  |  | |||
							
								
								
									
										99
									
								
								js/storage-util.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								js/storage-util.js
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,99 @@ | |||
| /* global LZString loadScript */ | ||||
| 'use strict'; | ||||
| 
 | ||||
| // eslint-disable-next-line no-var
 | ||||
| var [chromeLocal, chromeSync] = [ | ||||
|   chrome.storage.local, | ||||
|   chrome.storage.sync, | ||||
| ].map(storage => { | ||||
|   const wrapper = { | ||||
|     get(options) { | ||||
|       return new Promise(resolve => { | ||||
|         storage.get(options, data => resolve(data)); | ||||
|       }); | ||||
|     }, | ||||
|     set(data) { | ||||
|       return new Promise(resolve => { | ||||
|         storage.set(data, () => resolve(data)); | ||||
|       }); | ||||
|     }, | ||||
|     remove(keyOrKeys) { | ||||
|       return new Promise(resolve => { | ||||
|         storage.remove(keyOrKeys, resolve); | ||||
|       }); | ||||
|     }, | ||||
|     getValue(key) { | ||||
|       return wrapper.get(key).then(data => data[key]); | ||||
|     }, | ||||
|     setValue(key, value) { | ||||
|       return wrapper.set({[key]: value}); | ||||
|     }, | ||||
|     loadLZStringScript() { | ||||
|       return Promise.resolve( | ||||
|         window.LZString || | ||||
|         loadScript('/vendor/lz-string/lz-string-unsafe.js').then(() => { | ||||
|           window.LZString = window.LZStringUnsafe; | ||||
|         })); | ||||
|     }, | ||||
|     getLZValue(key) { | ||||
|       return wrapper.getLZValues([key]).then(data => data[key]); | ||||
|     }, | ||||
|     getLZValues(keys) { | ||||
|       return Promise.all([ | ||||
|         wrapper.get(keys), | ||||
|         wrapper.loadLZStringScript(), | ||||
|       ]).then(([data = {}]) => { | ||||
|         for (const key of keys) { | ||||
|           const value = data[key]; | ||||
|           data[key] = value && tryJSONparse(LZString.decompressFromUTF16(value)); | ||||
|         } | ||||
|         return data; | ||||
|       }); | ||||
|     }, | ||||
|     setLZValue(key, value) { | ||||
|       return wrapper.loadLZStringScript().then(() => | ||||
|         wrapper.set({ | ||||
|           [key]: LZString.compressToUTF16(JSON.stringify(value)), | ||||
|         })); | ||||
|     } | ||||
|   }; | ||||
|   return wrapper; | ||||
| }); | ||||
| 
 | ||||
| 
 | ||||
| function styleSectionsEqual({sections: a}, {sections: b}) { | ||||
|   if (!a || !b) { | ||||
|     return undefined; | ||||
|   } | ||||
|   if (a.length !== b.length) { | ||||
|     return false; | ||||
|   } | ||||
|   // order of sections should be identical to account for the case of multiple
 | ||||
|   // sections matching the same URL because the order of rules is part of cascading
 | ||||
|   return a.every((sectionA, index) => propertiesEqual(sectionA, b[index])); | ||||
| 
 | ||||
|   function propertiesEqual(secA, secB) { | ||||
|     for (const name of ['urlPrefixes', 'urls', 'domains', 'regexps']) { | ||||
|       if (!equalOrEmpty(secA[name], secB[name], 'every', arrayMirrors)) { | ||||
|         return false; | ||||
|       } | ||||
|     } | ||||
|     return equalOrEmpty(secA.code, secB.code, 'substr', (a, b) => a === b); | ||||
|   } | ||||
| 
 | ||||
|   function equalOrEmpty(a, b, telltale, comparator) { | ||||
|     const typeA = a && typeof a[telltale] === 'function'; | ||||
|     const typeB = b && typeof b[telltale] === 'function'; | ||||
|     return ( | ||||
|       (a === null || a === undefined || (typeA && !a.length)) && | ||||
|       (b === null || b === undefined || (typeB && !b.length)) | ||||
|     ) || typeA && typeB && a.length === b.length && comparator(a, b); | ||||
|   } | ||||
| 
 | ||||
|   function arrayMirrors(array1, array2) { | ||||
|     return ( | ||||
|       array1.every(el => array2.includes(el)) && | ||||
|       array2.every(el => array1.includes(el)) | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										17
									
								
								manage.html
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								manage.html
									
									
									
									
									
								
							|  | @ -150,13 +150,18 @@ | |||
|   <script src="js/prefs.js"></script> | ||||
|   <script src="content/apply.js"></script> | ||||
|   <script src="js/localization.js"></script> | ||||
|   <script src="js/storage-util.js"></script> | ||||
|   <script src="manage/filters.js"></script> | ||||
|   <script src="manage/updater-ui.js"></script> | ||||
|   <script src="manage/object-diff.js"></script> | ||||
|   <script src="vendor-overwrites/colorpicker/colorpicker.js"></script> | ||||
|   <script src="manage/config-dialog.js"></script> | ||||
|   <script src="manage/sort.js"></script> | ||||
|   <script src="manage/manage.js"></script> | ||||
| 
 | ||||
|   <script src="vendor-overwrites/colorpicker/colorpicker.js" async></script> | ||||
|   <script src="manage/config-dialog.js" async></script> | ||||
|   <script src="manage/updater-ui.js" async></script> | ||||
|   <script src="manage/object-diff.js" async></script> | ||||
|   <script src="manage/import-export.js" async></script> | ||||
|   <script src="msgbox/msgbox.js" async></script> | ||||
|   <script src="manage/incremental-search.js" async></script> | ||||
| </head> | ||||
| 
 | ||||
| <body id="stylus-manage" i18n-dragndrop-hint="dragDropMessage"> | ||||
|  | @ -358,10 +363,6 @@ | |||
| 
 | ||||
| <div id="installed"></div> | ||||
| 
 | ||||
| <script src="manage/import-export.js"></script> | ||||
| <script src="msgbox/msgbox.js"></script> | ||||
| <script src="manage/incremental-search.js" async></script> | ||||
| 
 | ||||
| <svg xmlns="http://www.w3.org/2000/svg" style="display: none !important;"> | ||||
|   <symbol id="svg-icon-checked" viewBox="0 0 1000 1000"> | ||||
|     <path fill-rule="evenodd" d="M983.2,184.3L853,69.8c-4-3.5-9.3-5.3-14.5-5c-5.3,0.4-10.3,2.8-13.8,6.8L352.3,609.2L184.4,386.9c-3.2-4.2-8-7-13.2-7.8c-5.3-0.8-10.6,0.6-14.9,3.9L18,487.5c-8.8,6.7-10.6,19.3-3.9,28.1L325,927.2c3.6,4.8,9.3,7.7,15.3,8c0.2,0,0.5,0,0.7,0c5.8,0,11.3-2.5,15.1-6.8L985,212.6C992.3,204.3,991.5,191.6,983.2,184.3z"/> | ||||
|  |  | |||
|  | @ -107,7 +107,7 @@ function configDialog(style) { | |||
|     buttons.close.textContent = t(someDirty ? 'confirmCancel' : 'confirmClose'); | ||||
|   } | ||||
| 
 | ||||
|   function save({anyChangeIsDirty = false} = {}) { | ||||
|   function save({anyChangeIsDirty = false} = {}, bgStyle) { | ||||
|     if (saving) { | ||||
|       debounce(save, 0, ...arguments); | ||||
|       return; | ||||
|  | @ -116,11 +116,18 @@ function configDialog(style) { | |||
|         !vars.some(va => va.dirty || anyChangeIsDirty && va.value !== va.savedValue)) { | ||||
|       return; | ||||
|     } | ||||
|     if (!bgStyle) { | ||||
|       API.getStyles({id: style.id, omitCode: !BG}) | ||||
|         .then(([bgStyle]) => save({anyChangeIsDirty}, bgStyle || {})); | ||||
|       return; | ||||
|     } | ||||
|     style = style.sections ? Object.assign({}, style) : style; | ||||
|     style.enabled = true; | ||||
|     style.reason = 'config'; | ||||
|     style.sourceCode = null; | ||||
|     style.sections = null; | ||||
|     const styleVars = style.usercssData.vars; | ||||
|     const bgStyle = BG.cachedStyles.byId.get(style.id); | ||||
|     const bgVars = bgStyle && (bgStyle.usercssData || {}).vars || {}; | ||||
|     const bgVars = (bgStyle.usercssData || {}).vars || {}; | ||||
|     const invalid = []; | ||||
|     let numValid = 0; | ||||
|     for (const va of vars) { | ||||
|  | @ -164,9 +171,9 @@ function configDialog(style) { | |||
|       return; | ||||
|     } | ||||
|     saving = true; | ||||
|     return BG.usercssHelper.save(BG.deepCopy(style)) | ||||
|     return API.saveUsercss(style) | ||||
|       .then(saved => { | ||||
|         varsInitial = getInitialValues(deepCopy(saved.usercssData.vars)); | ||||
|         varsInitial = getInitialValues(saved.usercssData.vars); | ||||
|         vars.forEach(va => onchange({target: va.input, justSaved: true})); | ||||
|         renderValues(); | ||||
|         updateButtons(); | ||||
|  |  | |||
|  | @ -29,7 +29,7 @@ HTMLSelectElement.prototype.adjustWidth = function () { | |||
|   parent.replaceChild(this, singleSelect); | ||||
| }; | ||||
| 
 | ||||
| onDOMready().then(onBackgroundReady).then(() => { | ||||
| onDOMready().then(() => { | ||||
|   $('#search').oninput = searchStyles; | ||||
|   if (urlFilterParam) { | ||||
|     $('#search').value = 'url:' + urlFilterParam; | ||||
|  | @ -169,14 +169,17 @@ function filterAndAppend({entry, container}) { | |||
|     if (!filtersSelector.hide || !entry.matches(filtersSelector.hide)) { | ||||
|       entry.classList.add('hidden'); | ||||
|     } | ||||
|   } else if ($('#search').value.trim()) { | ||||
|     searchStyles({immediately: true, container}); | ||||
|   } | ||||
|   reapplyFilter(container); | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| function reapplyFilter(container = installed) { | ||||
| function reapplyFilter(container = installed, alreadySearched) { | ||||
|   if (!alreadySearched && $('#search').value.trim()) { | ||||
|     searchStyles({immediately: true, container}) | ||||
|       .then(() => reapplyFilter(container, true)); | ||||
|     return; | ||||
|   } | ||||
|   // A: show
 | ||||
|   let toHide = []; | ||||
|   let toUnhide = []; | ||||
|  | @ -189,9 +192,6 @@ function reapplyFilter(container = installed) { | |||
|   if (toUnhide instanceof DocumentFragment) { | ||||
|     installed.appendChild(toUnhide); | ||||
|     return; | ||||
|   } else if (toUnhide.length && $('#search').value.trim()) { | ||||
|     searchStyles({immediately: true, container: toUnhide}); | ||||
|     filterContainer({hide: false}); | ||||
|   } | ||||
|   // filtering needed or a single-element job from handleUpdate()
 | ||||
|   for (const entry of toUnhide.children || toUnhide) { | ||||
|  | @ -251,16 +251,12 @@ function reapplyFilter(container = installed) { | |||
| 
 | ||||
| 
 | ||||
| function showFiltersStats() { | ||||
|   if (!BG.cachedStyles.list) { | ||||
|     debounce(showFiltersStats, 100); | ||||
|     return; | ||||
|   } | ||||
|   const active = filtersSelector.hide !== ''; | ||||
|   $('#filters summary').classList.toggle('active', active); | ||||
|   $('#reset-filters').disabled = !active; | ||||
|   const numTotal = BG.cachedStyles.list.length; | ||||
|   const numTotal = installed.children.length; | ||||
|   const numHidden = installed.getElementsByClassName('entry hidden').length; | ||||
|   const numShown = Math.min(numTotal - numHidden, installed.children.length); | ||||
|   const numShown = numTotal - numHidden; | ||||
|   if (filtersSelector.numShown !== numShown || | ||||
|       filtersSelector.numTotal !== numTotal) { | ||||
|     filtersSelector.numShown = numShown; | ||||
|  | @ -273,87 +269,34 @@ function showFiltersStats() { | |||
| 
 | ||||
| 
 | ||||
| function searchStyles({immediately, container}) { | ||||
|   const searchElement = $('#search'); | ||||
|   const value = searchElement.value.trim(); | ||||
|   const urlMode = /^\s*url:/i.test(value); | ||||
|   const query = urlMode | ||||
|     ? value.replace(/^\s*url:/i, '') | ||||
|     : value.toLocaleLowerCase(); | ||||
|   if (query === searchElement.lastValue && !immediately && !container) { | ||||
|   const el = $('#search'); | ||||
|   const query = el.value.trim(); | ||||
|   if (query === el.lastValue && !immediately && !container) { | ||||
|     return; | ||||
|   } | ||||
|   if (!immediately) { | ||||
|     debounce(searchStyles, 150, {immediately: true}); | ||||
|     return; | ||||
|   } | ||||
|   searchElement.lastValue = query; | ||||
|   el.lastValue = query; | ||||
| 
 | ||||
|   const rx = query.startsWith('/') && query.indexOf('/', 1) > 0 && | ||||
|     tryRegExp(...(value.match(/^\s*\/(.*?)\/([gimsuy]*)\s*$/) || []).slice(1)); | ||||
|   const words = rx ? null : | ||||
|     query.startsWith('"') && query.endsWith('"') ? [value.trim().slice(1, -1)] : | ||||
|       query.split(/\s+/).filter(s => s.length > 1); | ||||
|   if (words && !words.length) { | ||||
|     words.push(query); | ||||
|   } | ||||
|   const entries = container && container.children || container || installed.children; | ||||
|   const siteStyleIds = urlMode && | ||||
|     new Set(BG.filterStyles({matchUrl: query}).map(style => style.id)); | ||||
|   let needsRefilter = false; | ||||
|   for (const entry of entries) { | ||||
|     let isMatching = !query || words && !words.length; | ||||
|     if (!isMatching) { | ||||
|       const style = urlMode ? siteStyleIds.has(entry.styleId) : | ||||
|         BG.cachedStyles.byId.get(entry.styleId) || {}; | ||||
|       isMatching = Boolean(style && ( | ||||
|         urlMode || | ||||
|         isMatchingText(style.name) || | ||||
|         style.url && isMatchingText(style.url) || | ||||
|         style.sourceCode && isMatchingText(style.sourceCode) || | ||||
|         isMatchingStyle(style))); | ||||
|     } | ||||
|     if (entry.classList.contains('not-matching') !== !isMatching) { | ||||
|       entry.classList.toggle('not-matching', !isMatching); | ||||
|       needsRefilter = true; | ||||
|     } | ||||
|   } | ||||
|   if (needsRefilter && !container) { | ||||
|     filterOnChange({forceRefilter: true}); | ||||
|   } | ||||
|   return; | ||||
| 
 | ||||
|   function isMatchingStyle(style) { | ||||
|     for (const section of style.sections) { | ||||
|       for (const prop in section) { | ||||
|         const value = section[prop]; | ||||
|         switch (typeof value) { | ||||
|           case 'string': | ||||
|             if (isMatchingText(value)) { | ||||
|               return true; | ||||
|             } | ||||
|             break; | ||||
|           case 'object': | ||||
|             for (const str of value) { | ||||
|               if (isMatchingText(str)) { | ||||
|                 return true; | ||||
|               } | ||||
|             } | ||||
|             break; | ||||
|         } | ||||
|   return API.searchDB({ | ||||
|     query, | ||||
|     ids: [...entries].map(el => el.styleId), | ||||
|   }).then(ids => { | ||||
|     ids = new Set(ids); | ||||
|     let needsRefilter = false; | ||||
|     for (const entry of entries) { | ||||
|       const isMatching = ids.has(entry.styleId); | ||||
|       if (entry.classList.contains('not-matching') !== !isMatching) { | ||||
|         entry.classList.toggle('not-matching', !isMatching); | ||||
|         needsRefilter = true; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   function isMatchingText(text) { | ||||
|     if (rx) { | ||||
|       return rx.test(text); | ||||
|     if (needsRefilter && !container) { | ||||
|       filterOnChange({forceRefilter: true}); | ||||
|     } | ||||
|     for (let pass = 1; pass <= 2; pass++) { | ||||
|       if (words.every(word => text.includes(word))) { | ||||
|         return true; | ||||
|       } | ||||
|       text = text.toLocaleLowerCase(); | ||||
|     } | ||||
|     return false; | ||||
|   } | ||||
|     return container; | ||||
|   }); | ||||
| } | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| /* global messageBox, handleUpdate, applyOnMessage */ | ||||
| /* global messageBox handleUpdate applyOnMessage styleSectionsEqual */ | ||||
| 'use strict'; | ||||
| 
 | ||||
| const STYLISH_DUMP_FILE_EXT = '.txt'; | ||||
|  | @ -41,7 +41,7 @@ function importFromFile({fileTypeFilter, file} = {}) { | |||
|             importFromString(text) : | ||||
|             getOwnTab().then(tab => { | ||||
|               tab.url = URL.createObjectURL(new Blob([text], {type: 'text/css'})); | ||||
|               return BG.usercssHelper.openInstallPage(tab, {direct: true}) | ||||
|               return API.installUsercss({direct: true}, {tab}) | ||||
|                 .then(() => URL.revokeObjectURL(tab.url)); | ||||
|             }) | ||||
|           ).then(numStyles => { | ||||
|  | @ -56,17 +56,17 @@ function importFromFile({fileTypeFilter, file} = {}) { | |||
| } | ||||
| 
 | ||||
| 
 | ||||
| function importFromString(jsonString) { | ||||
|   if (!BG) { | ||||
|     onBackgroundReady().then(() => importFromString(jsonString)); | ||||
| function importFromString(jsonString, oldStyles) { | ||||
|   if (!oldStyles) { | ||||
|     API.getStyles().then(styles => importFromString(jsonString, styles)); | ||||
|     return; | ||||
|   } | ||||
|   // create objects in background context
 | ||||
|   const json = BG.tryJSONparse(jsonString) || []; | ||||
|   const json = tryJSONparse(jsonString) || []; | ||||
|   if (typeof json.slice !== 'function') { | ||||
|     json.length = 0; | ||||
|   } | ||||
|   const oldStyles = json.length && BG.deepCopy(BG.cachedStyles.list || []); | ||||
|   const oldStylesById = new Map( | ||||
|     oldStyles.map(style => [style.id, style])); | ||||
|   const oldStylesByName = json.length && new Map( | ||||
|     oldStyles.map(style => [style.name.trim(), style])); | ||||
| 
 | ||||
|  | @ -94,7 +94,7 @@ function importFromString(jsonString) { | |||
|       const info = analyze(item); | ||||
|       if (info) { | ||||
|         // using saveStyle directly since json was parsed in background page context
 | ||||
|         return BG.saveStyle(Object.assign(item, SAVE_OPTIONS)) | ||||
|         return API.saveStyle(Object.assign(item, SAVE_OPTIONS)) | ||||
|           .then(style => account({style, info, resolve})); | ||||
|       } | ||||
|     } | ||||
|  | @ -110,7 +110,7 @@ function importFromString(jsonString) { | |||
|       return; | ||||
|     } | ||||
|     item.name = item.name.trim(); | ||||
|     const byId = BG.cachedStyles.byId.get(item.id); | ||||
|     const byId = oldStylesById.get(item.id); | ||||
|     const byName = oldStylesByName.get(item.name); | ||||
|     oldStylesByName.delete(item.name); | ||||
|     let oldStyle; | ||||
|  | @ -129,7 +129,7 @@ function importFromString(jsonString) { | |||
|     const metaEqual = oldStyleKeys && | ||||
|       oldStyleKeys.length === Object.keys(item).length && | ||||
|       oldStyleKeys.every(k => k === 'sections' || oldStyle[k] === item[k]); | ||||
|     const codeEqual = oldStyle && BG.styleSectionsEqual(oldStyle, item); | ||||
|     const codeEqual = oldStyle && styleSectionsEqual(oldStyle, item); | ||||
|     if (metaEqual && codeEqual) { | ||||
|       stats.unchanged.names.push(oldStyle.name); | ||||
|       stats.unchanged.ids.push(oldStyle.id); | ||||
|  | @ -237,10 +237,10 @@ function importFromString(jsonString) { | |||
|         return; | ||||
|       } | ||||
|       const id = newIds[index++]; | ||||
|       deleteStyleSafe({id, notify: false}).then(id => { | ||||
|       API.deleteStyle({id, notify: false}).then(id => { | ||||
|         const oldStyle = oldStylesById.get(id); | ||||
|         if (oldStyle) { | ||||
|           saveStyleSafe(Object.assign(oldStyle, SAVE_OPTIONS)) | ||||
|           API.saveStyle(Object.assign(oldStyle, SAVE_OPTIONS)) | ||||
|             .then(undoNextId); | ||||
|         } else { | ||||
|           undoNextId(); | ||||
|  | @ -293,7 +293,7 @@ function importFromString(jsonString) { | |||
|     chrome.webNavigation.getAllFrames({tabId}, frames => { | ||||
|       frames = frames && frames[0] ? frames : [{frameId: 0}]; | ||||
|       frames.forEach(({frameId}) => | ||||
|         getStylesSafe({matchUrl: tab.url, enabled: true, asHash: true}).then(styles => { | ||||
|         API.getStyles({matchUrl: tab.url, enabled: true, asHash: true}).then(styles => { | ||||
|           const message = {method: 'styleReplaceAll', tabId, frameId, styles}; | ||||
|           if (tab.id === ownTab.id) { | ||||
|             applyOnMessage(message); | ||||
|  | @ -301,7 +301,7 @@ function importFromString(jsonString) { | |||
|             invokeOrPostpone(tab.active, sendMessage, message, ignoreChromeError); | ||||
|           } | ||||
|           if (frameId === 0) { | ||||
|             setTimeout(BG.updateIcon, 0, tab, styles); | ||||
|             setTimeout(API.updateIcon, 0, tab, styles); | ||||
|           } | ||||
|         })); | ||||
|       if (resolve) { | ||||
|  | @ -314,7 +314,7 @@ function importFromString(jsonString) { | |||
| 
 | ||||
| 
 | ||||
| $('#file-all-styles').onclick = () => { | ||||
|   getStylesSafe().then(styles => { | ||||
|   API.getStyles().then(styles => { | ||||
|     const text = JSON.stringify(styles, null, '\t'); | ||||
|     const blob = new Blob([text], {type: 'application/json'}); | ||||
|     const objectURL = URL.createObjectURL(blob); | ||||
|  |  | |||
|  | @ -1,9 +1,11 @@ | |||
| /* global messageBox, getStyleWithNoCode, retranslateCSS */ | ||||
| /* global filtersSelector, filterAndAppend */ | ||||
| /* global checkUpdate, handleUpdateInstalled */ | ||||
| /* global objectDiff */ | ||||
| /* global configDialog */ | ||||
| /* global sorter */ | ||||
| /* | ||||
| global messageBox getStyleWithNoCode retranslateCSS | ||||
| global filtersSelector filterAndAppend urlFilterParam | ||||
| global checkUpdate handleUpdateInstalled | ||||
| global objectDiff | ||||
| global configDialog | ||||
| global sorter | ||||
| */ | ||||
| 'use strict'; | ||||
| 
 | ||||
| let installed; | ||||
|  | @ -30,14 +32,13 @@ const OWN_ICON = chrome.runtime.getManifest().icons['16']; | |||
| const handleEvent = {}; | ||||
| 
 | ||||
| Promise.all([ | ||||
|   getStylesSafe(), | ||||
|   API.getStyles({omitCode: !BG}), | ||||
|   urlFilterParam && API.searchDB({query: 'url:' + urlFilterParam}), | ||||
|   onDOMready().then(initGlobalEvents), | ||||
| ]).then(([styles]) => { | ||||
|   showStyles(styles); | ||||
| ]).then(args => { | ||||
|   showStyles(...args); | ||||
| }); | ||||
| 
 | ||||
| dieOnNullBackground(); | ||||
| 
 | ||||
| chrome.runtime.onMessage.addListener(onRuntimeMessage); | ||||
| 
 | ||||
| function onRuntimeMessage(msg) { | ||||
|  | @ -107,7 +108,7 @@ function initGlobalEvents() { | |||
| } | ||||
| 
 | ||||
| 
 | ||||
| function showStyles(styles = []) { | ||||
| function showStyles(styles = [], matchUrlIds) { | ||||
|   const sorted = sorter.sort({ | ||||
|     styles: styles.map(style => ({ | ||||
|       style, | ||||
|  | @ -137,7 +138,13 @@ function showStyles(styles = []) { | |||
|       // eslint-disable-next-line no-unmodified-loop-condition
 | ||||
|       (shouldRenderAll || ++rendered < 20 || performance.now() - t0 < 10) | ||||
|     ) { | ||||
|       renderBin.appendChild(createStyleElement(sorted[index++])); | ||||
|       const info = sorted[index++]; | ||||
|       const entry = createStyleElement(info); | ||||
|       if (matchUrlIds && !matchUrlIds.includes(info.style.id)) { | ||||
|         entry.classList.add('not-matching'); | ||||
|         rendered--; | ||||
|       } | ||||
|       renderBin.appendChild(entry); | ||||
|     } | ||||
|     filterAndAppend({container: renderBin}); | ||||
|     if (index < sorted.length) { | ||||
|  | @ -277,7 +284,7 @@ function createStyleTargetsElement({entry, style, iconsOnly}) { | |||
| 
 | ||||
| 
 | ||||
| function recreateStyleTargets({styles, iconsOnly = false} = {}) { | ||||
|   Promise.resolve(styles || getStylesSafe()).then(styles => { | ||||
|   Promise.resolve(styles || API.getStyles()).then(styles => { | ||||
|     for (const style of styles) { | ||||
|       const entry = $(ENTRY_ID_PREFIX + style.id); | ||||
|       if (entry) { | ||||
|  | @ -391,7 +398,7 @@ Object.assign(handleEvent, { | |||
|   }, | ||||
| 
 | ||||
|   toggle(event, entry) { | ||||
|     saveStyleSafe({ | ||||
|     API.saveStyle({ | ||||
|       id: entry.styleId, | ||||
|       enabled: this.matches('.enable') || this.checked, | ||||
|     }); | ||||
|  | @ -399,39 +406,30 @@ Object.assign(handleEvent, { | |||
| 
 | ||||
|   check(event, entry) { | ||||
|     event.preventDefault(); | ||||
|     checkUpdate(entry); | ||||
|     checkUpdate(entry, {single: true}); | ||||
|   }, | ||||
| 
 | ||||
|   update(event, entry) { | ||||
|     event.preventDefault(); | ||||
|     const request = Object.assign(entry.updatedCode, { | ||||
|       id: entry.styleId, | ||||
|       reason: 'update', | ||||
|     }); | ||||
|     if (entry.updatedCode.usercssData) { | ||||
|       onBackgroundReady() | ||||
|         .then(() => BG.usercssHelper.save(request)); | ||||
|     } else { | ||||
|       // update everything but name
 | ||||
|       request.name = null; | ||||
|       saveStyleSafe(request); | ||||
|     } | ||||
|     const json = entry.updatedCode; | ||||
|     json.id = entry.styleId; | ||||
|     json.reason = 'update'; | ||||
|     API[json.usercssData ? 'saveUsercss' : 'saveStyle'](json); | ||||
|   }, | ||||
| 
 | ||||
|   delete(event, entry) { | ||||
|     event.preventDefault(); | ||||
|     const id = entry.styleId; | ||||
|     const {name} = BG.cachedStyles.byId.get(id) || {}; | ||||
|     animateElement(entry); | ||||
|     messageBox({ | ||||
|       title: t('deleteStyleConfirm'), | ||||
|       contents: name, | ||||
|       contents: entry.styleMeta.name, | ||||
|       className: 'danger center', | ||||
|       buttons: [t('confirmDelete'), t('confirmCancel')], | ||||
|     }) | ||||
|     .then(({button}) => { | ||||
|       if (button === 0) { | ||||
|         deleteStyleSafe({id}); | ||||
|         API.deleteStyle({id}); | ||||
|       } | ||||
|     }); | ||||
|   }, | ||||
|  | @ -525,7 +523,7 @@ function handleUpdate(style, {reason, method} = {}) { | |||
|   sorter.update(); | ||||
|   if (!entry.matches('.hidden') && reason !== 'import') { | ||||
|     animateElement(entry); | ||||
|     scrollElementIntoView(entry); | ||||
|     requestAnimationFrame(() => scrollElementIntoView(entry)); | ||||
|   } | ||||
| 
 | ||||
|   function handleToggledOrCodeOnly() { | ||||
|  | @ -606,7 +604,7 @@ function switchUI({styleOnly} = {}) { | |||
|   const missingFavicons = newUI.enabled && newUI.favicons && !$('.applies-to img'); | ||||
|   if (changed.enabled || (missingFavicons && !createStyleElement.parts)) { | ||||
|     installed.textContent = ''; | ||||
|     getStylesSafe().then(showStyles); | ||||
|     API.getStyles().then(showStyles); | ||||
|     return; | ||||
|   } | ||||
|   if (changed.targets) { | ||||
|  | @ -645,28 +643,3 @@ function usePrefsDuringPageLoad() { | |||
|   } | ||||
|   $$('#header select').forEach(el => el.adjustWidth()); | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| // TODO: remove when these bugs are fixed in FF
 | ||||
| function dieOnNullBackground() { | ||||
|   if (!FIREFOX || BG) { | ||||
|     return; | ||||
|   } | ||||
|   sendMessage({method: 'healthCheck'}, health => { | ||||
|     if (health && !chrome.extension.getBackgroundPage()) { | ||||
|       onDOMready().then(() => { | ||||
|         sendMessage({method: 'getStyles'}, showStyles); | ||||
|         messageBox({ | ||||
|           title: 'Stylus', | ||||
|           className: 'danger center', | ||||
|           contents: t('dysfunctionalBackgroundConnection'), | ||||
|           onshow: () => { | ||||
|             $('#message-box-close-icon').remove(); | ||||
|             window.removeEventListener('keydown', messageBox.listeners.key, true); | ||||
|           } | ||||
|         }); | ||||
|         document.documentElement.style.pointerEvents = 'none'; | ||||
|       }); | ||||
|     } | ||||
|   }); | ||||
| } | ||||
|  |  | |||
|  | @ -129,7 +129,7 @@ const sorter = (() => { | |||
|       styles: current.map(entry => ({ | ||||
|         entry, | ||||
|         name: entry.styleNameLowerCase + '\n' + entry.styleMeta.name, | ||||
|         style: BG.cachedStyles.byId.get(entry.styleId), | ||||
|         style: entry.styleMeta, | ||||
|       })) | ||||
|     }); | ||||
|     if (current.some((entry, index) => entry !== sorted[index].entry)) { | ||||
|  |  | |||
|  | @ -29,41 +29,51 @@ function applyUpdateAll() { | |||
| 
 | ||||
| function checkUpdateAll() { | ||||
|   document.body.classList.add('update-in-progress'); | ||||
|   $('#check-all-updates').disabled = true; | ||||
|   $('#check-all-updates-force').classList.add('hidden'); | ||||
|   $('#apply-all-updates').classList.add('hidden'); | ||||
|   $('#update-all-no-updates').classList.add('hidden'); | ||||
|   const btnCheck = $('#check-all-updates'); | ||||
|   const btnCheckForce = $('#check-all-updates-force'); | ||||
|   const btnApply = $('#apply-all-updates'); | ||||
|   const noUpdates = $('#update-all-no-updates'); | ||||
|   btnCheck.disabled = true; | ||||
|   btnCheckForce.classList.add('hidden'); | ||||
|   btnApply.classList.add('hidden'); | ||||
|   noUpdates.classList.add('hidden'); | ||||
| 
 | ||||
|   const ignoreDigest = this && this.id === 'check-all-updates-force'; | ||||
|   $$('.updatable:not(.can-update)' + (ignoreDigest ? '' : ':not(.update-problem)')) | ||||
|     .map(el => checkUpdate(el, {single: false})); | ||||
|     .map(checkUpdate); | ||||
| 
 | ||||
|   let total = 0; | ||||
|   let checked = 0; | ||||
|   let skippedEdited = 0; | ||||
|   let updated = 0; | ||||
| 
 | ||||
|   BG.updater.checkAllStyles({observer, save: false, ignoreDigest}).then(done); | ||||
|   chrome.runtime.onConnect.addListener(function onConnect(port) { | ||||
|     if (port.name !== 'updater') return; | ||||
|     port.onMessage.addListener(observer); | ||||
|     chrome.runtime.onConnect.removeListener(onConnect); | ||||
|   }); | ||||
| 
 | ||||
|   function observer(state, value, details) { | ||||
|     switch (state) { | ||||
|       case BG.updater.COUNT: | ||||
|         total = value; | ||||
|         break; | ||||
|       case BG.updater.UPDATED: | ||||
|         if (++updated === 1) { | ||||
|           $('#apply-all-updates').disabled = true; | ||||
|           $('#apply-all-updates').classList.remove('hidden'); | ||||
|         } | ||||
|         $('#apply-all-updates').dataset.value = updated; | ||||
|         // fallthrough
 | ||||
|       case BG.updater.SKIPPED: | ||||
|         checked++; | ||||
|         if (details === BG.updater.EDITED || details === BG.updater.MAYBE_EDITED) { | ||||
|           skippedEdited++; | ||||
|         } | ||||
|         reportUpdateState(state, value, details); | ||||
|         break; | ||||
|   API.updateCheckAll({ | ||||
|     save: false, | ||||
|     observe: true, | ||||
|     ignoreDigest, | ||||
|   }).then(done); | ||||
| 
 | ||||
|   function observer(info) { | ||||
|     if ('count' in info) { | ||||
|       total = info.count; | ||||
|     } | ||||
|     if (info.updated) { | ||||
|       if (++updated === 1) { | ||||
|         btnApply.disabled = true; | ||||
|         btnApply.classList.remove('hidden'); | ||||
|       } | ||||
|       btnApply.dataset.value = updated; | ||||
|     } | ||||
|     if (info.updated || info.error) { | ||||
|       checked++; | ||||
|       skippedEdited += [info.STATES.EDITED, info.STATES.MAYBE_EDITED].includes(info.error); | ||||
|       reportUpdateState(info); | ||||
|     } | ||||
|     const progress = $('#update-progress'); | ||||
|     const maxWidth = progress.parentElement.clientWidth; | ||||
|  | @ -72,35 +82,34 @@ function checkUpdateAll() { | |||
| 
 | ||||
|   function done() { | ||||
|     document.body.classList.remove('update-in-progress'); | ||||
|     $('#check-all-updates').disabled = total === 0; | ||||
|     $('#apply-all-updates').disabled = false; | ||||
|     btnCheck.disabled = total === 0; | ||||
|     btnApply.disabled = false; | ||||
|     renderUpdatesOnlyFilter({check: updated + skippedEdited > 0}); | ||||
|     if (!updated) { | ||||
|       $('#update-all-no-updates').dataset.skippedEdited = skippedEdited > 0; | ||||
|       $('#update-all-no-updates').classList.remove('hidden'); | ||||
|       $('#check-all-updates-force').classList.toggle('hidden', skippedEdited === 0); | ||||
|       noUpdates.dataset.skippedEdited = skippedEdited > 0; | ||||
|       noUpdates.classList.remove('hidden'); | ||||
|       btnCheckForce.classList.toggle('hidden', skippedEdited === 0); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| function checkUpdate(entry, {single = true} = {}) { | ||||
| function checkUpdate(entry, {single} = {}) { | ||||
|   $('.update-note', entry).textContent = t('checkingForUpdate'); | ||||
|   $('.check-update', entry).title = ''; | ||||
|   if (single) { | ||||
|     BG.updater.checkStyle({ | ||||
|     API.updateCheck({ | ||||
|       save: false, | ||||
|       id: entry.styleId, | ||||
|       ignoreDigest: entry.classList.contains('update-problem'), | ||||
|       style: BG.cachedStyles.byId.get(entry.styleId), | ||||
|       observer: reportUpdateState, | ||||
|     }); | ||||
|     }).then(reportUpdateState); | ||||
|   } | ||||
|   entry.classList.remove('checking-update', 'no-update', 'update-problem'); | ||||
|   entry.classList.add('checking-update'); | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| function reportUpdateState(state, style, details) { | ||||
| function reportUpdateState({updated, style, error, STATES}) { | ||||
|   const entry = $(ENTRY_ID_PREFIX + style.id); | ||||
|   const newClasses = new Map([ | ||||
|     /* | ||||
|  | @ -117,43 +126,37 @@ function reportUpdateState(state, style, details) { | |||
|     ['no-update', 0], | ||||
|     ['update-problem', 0], | ||||
|   ]); | ||||
|   switch (state) { | ||||
|     case BG.updater.UPDATED: | ||||
|       newClasses.set('can-update', true); | ||||
|       entry.updatedCode = style; | ||||
|       $('.update-note', entry).textContent = ''; | ||||
|       $('#only-updates').classList.remove('hidden'); | ||||
|       break; | ||||
|     case BG.updater.SKIPPED: { | ||||
|       if (entry.classList.contains('can-update')) { | ||||
|         break; | ||||
|       } | ||||
|       const same = ( | ||||
|         details === BG.updater.SAME_MD5 || | ||||
|         details === BG.updater.SAME_CODE || | ||||
|         details === BG.updater.SAME_VERSION | ||||
|       ); | ||||
|       const edited = details === BG.updater.EDITED || details === BG.updater.MAYBE_EDITED; | ||||
|       entry.dataset.details = details; | ||||
|       if (!details) { | ||||
|         details = t('updateCheckFailServerUnreachable') + '\n' + style.updateUrl; | ||||
|       } else if (typeof details === 'number') { | ||||
|         details = t('updateCheckFailBadResponseCode', [details]) + '\n' + style.updateUrl; | ||||
|       } else if (details === BG.updater.EDITED) { | ||||
|         details = t('updateCheckSkippedLocallyEdited') + '\n' + t('updateCheckManualUpdateHint'); | ||||
|       } else if (details === BG.updater.MAYBE_EDITED) { | ||||
|         details = t('updateCheckSkippedMaybeLocallyEdited') + '\n' + t('updateCheckManualUpdateHint'); | ||||
|       } | ||||
|       const message = same ? t('updateCheckSucceededNoUpdate') : details; | ||||
|       newClasses.set('no-update', true); | ||||
|       newClasses.set('update-problem', !same); | ||||
|       $('.update-note', entry).textContent = message; | ||||
|       $('.check-update', entry).title = newUI.enabled ? message : ''; | ||||
|       $('.update', entry).title = t(edited ? 'updateCheckManualUpdateForce' : 'installUpdate'); | ||||
|       if (!document.body.classList.contains('update-in-progress')) { | ||||
|         // this is a single update job so we can decide whether to hide the filter
 | ||||
|         renderUpdatesOnlyFilter({show: $('.can-update, .update-problem')}); | ||||
|       } | ||||
|   if (updated) { | ||||
|     newClasses.set('can-update', true); | ||||
|     entry.updatedCode = style; | ||||
|     $('.update-note', entry).textContent = ''; | ||||
|     $('#only-updates').classList.remove('hidden'); | ||||
|   } else if (!entry.classList.contains('can-update')) { | ||||
|     const same = ( | ||||
|       error === STATES.SAME_MD5 || | ||||
|       error === STATES.SAME_CODE || | ||||
|       error === STATES.SAME_VERSION | ||||
|     ); | ||||
|     const edited = error === STATES.EDITED || error === STATES.MAYBE_EDITED; | ||||
|     entry.dataset.error = error; | ||||
|     if (!error) { | ||||
|       error = t('updateCheckFailServerUnreachable') + '\n' + style.updateUrl; | ||||
|     } else if (typeof error === 'number') { | ||||
|       error = t('updateCheckFailBadResponseCode', [error]) + '\n' + style.updateUrl; | ||||
|     } else if (error === STATES.EDITED) { | ||||
|       error = t('updateCheckSkippedLocallyEdited') + '\n' + t('updateCheckManualUpdateHint'); | ||||
|     } else if (error === STATES.MAYBE_EDITED) { | ||||
|       error = t('updateCheckSkippedMaybeLocallyEdited') + '\n' + t('updateCheckManualUpdateHint'); | ||||
|     } | ||||
|     const message = same ? t('updateCheckSucceededNoUpdate') : error; | ||||
|     newClasses.set('no-update', true); | ||||
|     newClasses.set('update-problem', !same); | ||||
|     $('.update-note', entry).textContent = message; | ||||
|     $('.check-update', entry).title = newUI.enabled ? message : ''; | ||||
|     $('.update', entry).title = t(edited ? 'updateCheckManualUpdateForce' : 'installUpdate'); | ||||
|     if (!document.body.classList.contains('update-in-progress')) { | ||||
|       // this is a single update job so we can decide whether to hide the filter
 | ||||
|       renderUpdatesOnlyFilter({show: $('.can-update, .update-problem')}); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|  | @ -162,9 +165,16 @@ function reportUpdateState(state, style, details) { | |||
|   // 2. remove falsy newClasses
 | ||||
|   // 3. keep existing classes otherwise
 | ||||
|   const classes = new Map([...entry.classList.values()].map(cls => [cls, true])); | ||||
|   [...newClasses.entries()].forEach(([cls, newState]) => classes.set(cls, newState)); | ||||
|   const className = [...classes.entries()].filter(([, state]) => state).map(([cls]) => cls).join(' '); | ||||
|   if (className !== entry.className) entry.className = className; | ||||
|   for (const [cls, newState] of newClasses.entries()) { | ||||
|     classes.set(cls, newState); | ||||
|   } | ||||
|   const className = [...classes.entries()] | ||||
|     .map(([cls, state]) => state && cls) | ||||
|     .filter(Boolean) | ||||
|     .join(' '); | ||||
|   if (className !== entry.className) { | ||||
|     entry.className = className; | ||||
|   } | ||||
| 
 | ||||
|   if (filtersSelector.hide) { | ||||
|     filterAndAppend({entry}); | ||||
|  | @ -200,7 +210,10 @@ function showUpdateHistory(event) { | |||
|   const log = $create('.update-history-log'); | ||||
|   let logText, scroller, toggler; | ||||
|   let deleted = false; | ||||
|   BG.chromeLocal.getValue('updateLog').then((lines = []) => { | ||||
|   Promise.all([ | ||||
|     chromeLocal.getValue('updateLog'), | ||||
|     API.getUpdaterStates(), | ||||
|   ]).then(([lines = [], states]) => { | ||||
|     logText = lines.join('\n'); | ||||
|     messageBox({ | ||||
|       title: t('updateCheckHistory'), | ||||
|  | @ -227,6 +240,13 @@ function showUpdateHistory(event) { | |||
|             t('manageOnlyUpdates'), | ||||
|           ])); | ||||
| 
 | ||||
|         toggler.rxRemoveNOP = new RegExp( | ||||
|           '^[^#]*(' + | ||||
|           Object.keys(states) | ||||
|             .filter(k => k.startsWith('SAME_')) | ||||
|             .map(k => states[k]) | ||||
|             .join('|') + | ||||
|           ').*\r?\n', 'gm'); | ||||
|         toggler.onchange(); | ||||
|       }), | ||||
|     }); | ||||
|  | @ -242,26 +262,17 @@ function showUpdateHistory(event) { | |||
|       return; | ||||
|     } | ||||
|     const scrollRatio = calcScrollRatio(); | ||||
|     const rxRemoveNOP = this.checked && new RegExp([ | ||||
|       '^[^#]*(', | ||||
|       Object.keys(BG.updater) | ||||
|         .filter(k => k.startsWith('SAME_')) | ||||
|         .map(k => stringAsRegExp(BG.updater[k])) | ||||
|         .map(rx => rx.source) | ||||
|         .join('|'), | ||||
|       ').*\r?\n', | ||||
|     ].join(''), 'gm'); | ||||
|     log.textContent = !this.checked ? logText : logText.replace(rxRemoveNOP, ''); | ||||
|     log.textContent = !this.checked ? logText : logText.replace(this.rxRemoveNOP, ''); | ||||
|     if (Math.abs(scrollRatio - calcScrollRatio()) > .1) { | ||||
|       scroller.scrollTop = scrollRatio * scroller.scrollHeight - scroller.clientHeight; | ||||
|     } | ||||
|   } | ||||
|   function deleteHistory() { | ||||
|     if (deleted) { | ||||
|       BG.chromeLocal.setValue('updateLog', logText.split('\n')); | ||||
|       chromeLocal.setValue('updateLog', logText.split('\n')); | ||||
|       setTimeout(scrollToBottom); | ||||
|     } else { | ||||
|       BG.chromeLocal.remove('updateLog'); | ||||
|       chromeLocal.remove('updateLog'); | ||||
|       log.textContent = ''; | ||||
|     } | ||||
|     deleted = !deleted; | ||||
|  |  | |||
|  | @ -21,17 +21,18 @@ | |||
|   "background": { | ||||
|     "scripts": [ | ||||
|       "js/messaging.js", | ||||
|       "vendor/lz-string/lz-string-unsafe.js", | ||||
|       "js/color-parser.js", | ||||
|       "js/usercss.js", | ||||
|       "js/storage-util.js", | ||||
|       "background/storage.js", | ||||
|       "background/usercss-helper.js", | ||||
|       "js/prefs.js", | ||||
|       "js/script-loader.js", | ||||
|       "js/color-parser.js", | ||||
|       "js/usercss.js", | ||||
|       "background/background.js", | ||||
|       "vendor/node-semver/semver.js", | ||||
|       "background/usercss-helper.js", | ||||
|       "background/style-via-api.js", | ||||
|       "background/update.js" | ||||
|       "background/search-db.js", | ||||
|       "background/update.js", | ||||
|       "vendor/node-semver/semver.js" | ||||
|     ] | ||||
|   }, | ||||
|   "commands": { | ||||
|  |  | |||
|  | @ -62,23 +62,26 @@ function checkUpdates() { | |||
|   let checked = 0; | ||||
|   let updated = 0; | ||||
|   const maxWidth = $('#update-progress').parentElement.clientWidth; | ||||
|   BG.updater.checkAllStyles({observer}); | ||||
| 
 | ||||
|   function observer(state, value) { | ||||
|     switch (state) { | ||||
|       case BG.updater.COUNT: | ||||
|         total = value; | ||||
|         document.body.classList.add('update-in-progress'); | ||||
|         break; | ||||
|       case BG.updater.UPDATED: | ||||
|         updated++; | ||||
|         // fallthrough
 | ||||
|       case BG.updater.SKIPPED: | ||||
|         checked++; | ||||
|         break; | ||||
|       case BG.updater.DONE: | ||||
|         document.body.classList.remove('update-in-progress'); | ||||
|         return; | ||||
|   chrome.runtime.onConnect.addListener(function onConnect(port) { | ||||
|     if (port.name !== 'updater') return; | ||||
|     port.onMessage.addListener(observer); | ||||
|     chrome.runtime.onConnect.removeListener(onConnect); | ||||
|   }); | ||||
| 
 | ||||
|   API.updateCheckAll({observe: true}); | ||||
| 
 | ||||
|   function observer(info) { | ||||
|     if ('count' in info) { | ||||
|       total = info.count; | ||||
|       document.body.classList.add('update-in-progress'); | ||||
|     } else if (info.updated) { | ||||
|       updated++; | ||||
|       checked++; | ||||
|     } else if (info.error) { | ||||
|       checked++; | ||||
|     } else if (info.done) { | ||||
|       document.body.classList.remove('update-in-progress'); | ||||
|     } | ||||
|     $('#update-progress').style.width = Math.round(checked / total * maxWidth) + 'px'; | ||||
|     $('#updates-installed').dataset.value = updated || ''; | ||||
|  |  | |||
|  | @ -161,6 +161,8 @@ | |||
|   <script src="popup/popup.js"></script> | ||||
|   <script src="popup/search-results.js"></script> | ||||
|   <script src="popup/hotkeys.js"></script> | ||||
|   <script src="js/script-loader.js" async></script> | ||||
|   <script src="js/storage-util.js" async></script> | ||||
| </head> | ||||
| 
 | ||||
| <body id="stylus-popup"> | ||||
|  |  | |||
|  | @ -101,11 +101,15 @@ var hotkeys = (() => { | |||
|       entry = typeof entry === 'string' ? $('#' + entry) : entry; | ||||
|       if (!match && $('.checker', entry).checked !== enable || entry.classList.contains(match)) { | ||||
|         results.push(entry.id); | ||||
|         task = task.then(() => saveStyleSafe({ | ||||
|         task = task.then(() => API.saveStyle({ | ||||
|           id: entry.styleId, | ||||
|           enabled: enable, | ||||
|           notify: false, | ||||
|         })); | ||||
|         })).then(() => { | ||||
|           entry.classList.toggle('enabled', enable); | ||||
|           entry.classList.toggle('disabled', !enable); | ||||
|           $('.checker', entry).checked = enable; | ||||
|         }); | ||||
|       } | ||||
|     } | ||||
|     if (results.length) { | ||||
|  | @ -115,7 +119,7 @@ var hotkeys = (() => { | |||
|   } | ||||
| 
 | ||||
|   function refreshAllTabs() { | ||||
|     getStylesSafe({matchUrl: location.href, enabled: true, asHash: true}) | ||||
|     API.getStyles({matchUrl: location.href, enabled: true, asHash: true}) | ||||
|       .then(styles => applyOnMessage({method: 'styleReplaceAll', styles})); | ||||
|     queryTabs().then(tabs => | ||||
|       tabs.forEach(tab => (!FIREFOX || tab.width) && | ||||
|  | @ -127,11 +131,11 @@ var hotkeys = (() => { | |||
|     chrome.webNavigation.getAllFrames({tabId}, frames => { | ||||
|       frames = frames && frames[0] ? frames : [{frameId: 0}]; | ||||
|       frames.forEach(({frameId}) => | ||||
|         getStylesSafe({matchUrl: tab.url, enabled: true, asHash: true}).then(styles => { | ||||
|         API.getStyles({matchUrl: tab.url, enabled: true, asHash: true}).then(styles => { | ||||
|           const message = {method: 'styleReplaceAll', tabId, frameId, styles}; | ||||
|           invokeOrPostpone(tab.active, sendMessage, message, ignoreChromeError); | ||||
|           if (frameId === 0) { | ||||
|             setTimeout(BG.updateIcon, 0, tab, styles); | ||||
|             setTimeout(API.updateIcon, 0, {tab, styles}); | ||||
|           } | ||||
|         })); | ||||
|       ignoreChromeError(); | ||||
|  |  | |||
							
								
								
									
										190
									
								
								popup/popup.js
									
									
									
									
									
								
							
							
						
						
									
										190
									
								
								popup/popup.js
									
									
									
									
									
								
							|  | @ -15,16 +15,15 @@ getActiveTab().then(tab => | |||
|   FIREFOX && tab.url === 'about:blank' && tab.status === 'loading' | ||||
|   ? getTabRealURLFirefox(tab) | ||||
|   : getTabRealURL(tab) | ||||
| ).then(url => { | ||||
|   tabURL = URLS.supported(url) ? url : ''; | ||||
|   Promise.all([ | ||||
|     tabURL && getStylesSafe({matchUrl: tabURL}), | ||||
|     onDOMready().then(() => { | ||||
|       initPopup(tabURL); | ||||
|     }), | ||||
|   ]).then(([styles]) => { | ||||
|     showStyles(styles); | ||||
|   }); | ||||
| ).then(url => Promise.all([ | ||||
|   (tabURL = URLS.supported(url) ? url : '') && | ||||
|   API.getStyles({ | ||||
|     matchUrl: tabURL, | ||||
|     omitCode: !BG, | ||||
|   }), | ||||
|   onDOMready().then(initPopup), | ||||
| ])).then(([styles]) => { | ||||
|   showStyles(styles); | ||||
| }); | ||||
| 
 | ||||
| chrome.runtime.onMessage.addListener(onRuntimeMessage); | ||||
|  | @ -33,9 +32,7 @@ function onRuntimeMessage(msg) { | |||
|   switch (msg.method) { | ||||
|     case 'styleAdded': | ||||
|     case 'styleUpdated': | ||||
|       // notifyAllTabs sets msg.style's code to null so we have to get the actual style
 | ||||
|       // because we analyze its code in detectSloppyRegexps
 | ||||
|       handleUpdate(BG.cachedStyles.byId.get(msg.style.id)); | ||||
|       handleUpdate(msg.style); | ||||
|       break; | ||||
|     case 'styleDeleted': | ||||
|       handleDelete(msg.id); | ||||
|  | @ -76,7 +73,7 @@ function toggleSideBorders(state = prefs.get('popup.borders')) { | |||
| } | ||||
| 
 | ||||
| 
 | ||||
| function initPopup(url) { | ||||
| function initPopup() { | ||||
|   installed = $('#installed'); | ||||
| 
 | ||||
|   setPopupWidth(); | ||||
|  | @ -108,7 +105,7 @@ function initPopup(url) { | |||
|       installed); | ||||
|   } | ||||
| 
 | ||||
|   if (!url) { | ||||
|   if (!tabURL) { | ||||
|     document.body.classList.add('blocked'); | ||||
|     document.body.insertBefore(template.unavailableInfo, document.body.firstChild); | ||||
|     return; | ||||
|  | @ -153,10 +150,10 @@ function initPopup(url) { | |||
|   // For this URL
 | ||||
|   const urlLink = template.writeStyle.cloneNode(true); | ||||
|   Object.assign(urlLink, { | ||||
|     href: 'edit.html?url-prefix=' + encodeURIComponent(url), | ||||
|     title: `url-prefix("${url}")`, | ||||
|     href: 'edit.html?url-prefix=' + encodeURIComponent(tabURL), | ||||
|     title: `url-prefix("${tabURL}")`, | ||||
|     textContent: prefs.get('popup.breadcrumbs.usePath') | ||||
|       ? new URL(url).pathname.slice(1) | ||||
|       ? new URL(tabURL).pathname.slice(1) | ||||
|       // this URL
 | ||||
|       : t('writeStyleForURL').replace(/ /g, '\u00a0'), | ||||
|     onclick: handleEvent.openLink, | ||||
|  | @ -170,7 +167,7 @@ function initPopup(url) { | |||
|   matchTargets.appendChild(urlLink); | ||||
| 
 | ||||
|   // For domain
 | ||||
|   const domains = BG.getDomains(url); | ||||
|   const domains = getDomains(tabURL); | ||||
|   for (const domain of domains) { | ||||
|     const numParts = domain.length - domain.replace(/\./g, '').length + 1; | ||||
|     // Don't include TLD
 | ||||
|  | @ -193,6 +190,19 @@ function initPopup(url) { | |||
|     matchTargets.appendChild(matchTargets.removeChild(matchTargets.firstElementChild)); | ||||
|   } | ||||
|   writeStyle.appendChild(matchWrapper); | ||||
| 
 | ||||
|   function getDomains(url) { | ||||
|     let d = /.*?:\/*([^/:]+)|$/.exec(url)[1]; | ||||
|     if (!d || url.startsWith('file:')) { | ||||
|       return []; | ||||
|     } | ||||
|     const domains = [d]; | ||||
|     while (d.indexOf('.') !== -1) { | ||||
|       d = d.substring(d.indexOf('.') + 1); | ||||
|       domains.push(d); | ||||
|     } | ||||
|     return domains; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
|  | @ -213,34 +223,30 @@ function showStyles(styles) { | |||
|       : a.name.localeCompare(b.name) | ||||
|   )); | ||||
| 
 | ||||
|   let postponeDetect = false; | ||||
|   const t0 = performance.now(); | ||||
|   const container = document.createDocumentFragment(); | ||||
|   for (const style of styles) { | ||||
|     createStyleElement({style, container, postponeDetect}); | ||||
|     postponeDetect = postponeDetect || performance.now() - t0 > 100; | ||||
|   } | ||||
|   styles.forEach(style => createStyleElement({style, container})); | ||||
|   installed.appendChild(container); | ||||
|   setTimeout(detectSloppyRegexps, 100, styles); | ||||
| 
 | ||||
|   getStylesSafe({matchUrl: tabURL, strictRegexp: false}) | ||||
|     .then(unscreenedStyles => { | ||||
|       for (const unscreened of unscreenedStyles) { | ||||
|         if (!styles.includes(unscreened)) { | ||||
|           postponeDetect = postponeDetect || performance.now() - t0 > 100; | ||||
|           createStyleElement({ | ||||
|             style: Object.assign({appliedSections: [], postponeDetect}, unscreened), | ||||
|           }); | ||||
|         } | ||||
|   API.getStyles({ | ||||
|     matchUrl: tabURL, | ||||
|     strictRegexp: false, | ||||
|     omitCode: true, | ||||
|   }).then(unscreenedStyles => { | ||||
|     for (const style of unscreenedStyles) { | ||||
|       if (!styles.find(({id}) => id === style.id)) { | ||||
|         createStyleElement({style, check: true}); | ||||
|       } | ||||
|       window.dispatchEvent(new Event('showStyles:done')); | ||||
|     }); | ||||
|     } | ||||
|     window.dispatchEvent(new Event('showStyles:done')); | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| function createStyleElement({ | ||||
|   style, | ||||
|   check = false, | ||||
|   container = installed, | ||||
|   postponeDetect, | ||||
| }) { | ||||
|   const entry = template.style.cloneNode(true); | ||||
|   entry.setAttribute('style-id', style.id); | ||||
|  | @ -294,7 +300,7 @@ function createStyleElement({ | |||
|   $('.delete', entry).onclick = handleEvent.delete; | ||||
|   $('.configure', entry).onclick = handleEvent.configure; | ||||
| 
 | ||||
|   invokeOrPostpone(!postponeDetect, detectSloppyRegexps, {entry, style}); | ||||
|   if (check) detectSloppyRegexps([style]); | ||||
| 
 | ||||
|   const oldElement = $(ENTRY_ID_PREFIX + style.id); | ||||
|   if (oldElement) { | ||||
|  | @ -316,23 +322,24 @@ Object.assign(handleEvent, { | |||
|   }, | ||||
| 
 | ||||
|   name(event) { | ||||
|     this.checkbox.click(); | ||||
|     this.checkbox.dispatchEvent(new MouseEvent('click')); | ||||
|     event.preventDefault(); | ||||
|   }, | ||||
| 
 | ||||
|   toggle(event) { | ||||
|     saveStyleSafe({ | ||||
|     API.saveStyle({ | ||||
|       id: handleEvent.getClickedStyleId(event), | ||||
|       enabled: this.type === 'checkbox' ? this.checked : this.matches('.enable'), | ||||
|       enabled: this.matches('.enable') || this.checked, | ||||
|     }); | ||||
|   }, | ||||
| 
 | ||||
|   delete(event) { | ||||
|     const id = handleEvent.getClickedStyleId(event); | ||||
|     const entry = handleEvent.getClickedStyleElement(event); | ||||
|     const id = entry.styleId; | ||||
|     const box = $('#confirm'); | ||||
|     box.dataset.display = true; | ||||
|     box.style.cssText = ''; | ||||
|     $('b', box).textContent = (BG.cachedStyles.byId.get(id) || {}).name; | ||||
|     $('b', box).textContent = $('.style-name', entry).textContent; | ||||
|     $('[data-cmd="ok"]', box).focus(); | ||||
|     $('[data-cmd="ok"]', box).onclick = () => confirm(true); | ||||
|     $('[data-cmd="cancel"]', box).onclick = () => confirm(false); | ||||
|  | @ -350,18 +357,14 @@ Object.assign(handleEvent, { | |||
|         className: 'lights-on', | ||||
|         onComplete: () => (box.dataset.display = false), | ||||
|       }); | ||||
|       if (ok) { | ||||
|         deleteStyleSafe({id}).then(() => { | ||||
|           handleDelete(id); | ||||
|         }); | ||||
|       } | ||||
|       if (ok) API.deleteStyle({id}); | ||||
|     } | ||||
|   }, | ||||
| 
 | ||||
|   configure(event) { | ||||
|     const {styleId, styleIsUsercss} = handleEvent.getClickedStyleElement(event); | ||||
|     if (styleIsUsercss) { | ||||
|       getStylesSafe({id: styleId}).then(([style]) => { | ||||
|       API.getStyles({id: styleId}).then(([style]) => { | ||||
|         hotkeys.setState(false); | ||||
|         configDialog(deepCopy(style)).then(() => { | ||||
|           hotkeys.setState(true); | ||||
|  | @ -456,15 +459,22 @@ Object.assign(handleEvent, { | |||
| 
 | ||||
| function handleUpdate(style) { | ||||
|   if ($(ENTRY_ID_PREFIX + style.id)) { | ||||
|     createStyleElement({style}); | ||||
|     createStyleElement({style, check: true}); | ||||
|     return; | ||||
|   } | ||||
|   if (!tabURL) return; | ||||
|   // Add an entry when a new style for the current url is installed
 | ||||
|   if (tabURL && BG.getApplicableSections({style, matchUrl: tabURL, stopOnFirst: true}).length) { | ||||
|     document.body.classList.remove('blocked'); | ||||
|     $$.remove('.blocked-info, #no-styles'); | ||||
|     createStyleElement({style}); | ||||
|   } | ||||
|   API.getStyles({ | ||||
|     matchUrl: tabURL, | ||||
|     stopOnFirst: true, | ||||
|     omitCode: true, | ||||
|   }).then(([style]) => { | ||||
|     if (style) { | ||||
|       document.body.classList.remove('blocked'); | ||||
|       $$.remove('.blocked-info, #no-styles'); | ||||
|       createStyleElement({style, check: true}); | ||||
|     } | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
|  | @ -476,58 +486,28 @@ function handleDelete(id) { | |||
| } | ||||
| 
 | ||||
| 
 | ||||
| /* | ||||
|   According to CSS4 @document specification the entire URL must match. | ||||
|   Stylish-for-Chrome implemented it incorrectly since the very beginning. | ||||
|   We'll detect styles that abuse the bug by finding the sections that | ||||
|   would have been applied by Stylish but not by us as we follow the spec. | ||||
|   Additionally we'll check for invalid regexps. | ||||
| */ | ||||
| function detectSloppyRegexps({entry, style}) { | ||||
|   // make sure all regexps are compiled
 | ||||
|   const rxCache = BG.cachedStyles.regexps; | ||||
|   let hasRegExp = false; | ||||
|   for (const section of style.sections) { | ||||
|     for (const regexp of section.regexps) { | ||||
|       hasRegExp = true; | ||||
|       for (let pass = 1; pass <= 2; pass++) { | ||||
|         const cacheKey = pass === 1 ? regexp : BG.SLOPPY_REGEXP_PREFIX + regexp; | ||||
|         if (!rxCache.has(cacheKey)) { | ||||
|           // according to CSS4 @document specification the entire URL must match
 | ||||
|           const anchored = pass === 1 ? '^(?:' + regexp + ')$' : '^' + regexp + '$'; | ||||
|           // create in the bg context to avoid leaking of "dead objects"
 | ||||
|           const rx = BG.tryRegExp(anchored); | ||||
|           rxCache.set(cacheKey, rx || false); | ||||
|         } | ||||
| function detectSloppyRegexps(styles) { | ||||
|   API.detectSloppyRegexps({ | ||||
|     matchUrl: tabURL, | ||||
|     ids: styles.map(({id}) => id), | ||||
|   }).then(results => { | ||||
|     for (const {id, applied, skipped, hasInvalidRegexps} of results) { | ||||
|       const entry = $(ENTRY_ID_PREFIX + id); | ||||
|       if (!entry) continue; | ||||
|       if (!applied) { | ||||
|         entry.classList.add('not-applied'); | ||||
|         $('.style-name', entry).title = t('styleNotAppliedRegexpProblemTooltip'); | ||||
|       } | ||||
|       if (skipped || hasInvalidRegexps) { | ||||
|         entry.classList.toggle('regexp-partial', Boolean(skipped)); | ||||
|         entry.classList.toggle('regexp-invalid', Boolean(hasInvalidRegexps)); | ||||
|         const indicator = template.regexpProblemIndicator.cloneNode(true); | ||||
|         indicator.appendChild(document.createTextNode(entry.skipped || '!')); | ||||
|         indicator.onclick = handleEvent.indicator; | ||||
|         $('.main-controls', entry).appendChild(indicator); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   if (!hasRegExp) { | ||||
|     return; | ||||
|   } | ||||
|   const { | ||||
|     appliedSections = | ||||
|       BG.getApplicableSections({style, matchUrl: tabURL}), | ||||
|     wannabeSections = | ||||
|       BG.getApplicableSections({style, matchUrl: tabURL, strictRegexp: false}), | ||||
|   } = style; | ||||
| 
 | ||||
|   entry.hasInvalidRegexps = wannabeSections.some(section => | ||||
|     section.regexps.some(rx => !rxCache.has(rx))); | ||||
|   entry.sectionsSkipped = wannabeSections.length - appliedSections.length; | ||||
| 
 | ||||
|   if (!appliedSections.length) { | ||||
|     entry.classList.add('not-applied'); | ||||
|     $('.style-name', entry).title = t('styleNotAppliedRegexpProblemTooltip'); | ||||
|   } | ||||
|   if (entry.sectionsSkipped || entry.hasInvalidRegexps) { | ||||
|     entry.classList.toggle('regexp-partial', entry.sectionsSkipped); | ||||
|     entry.classList.toggle('regexp-invalid', entry.hasInvalidRegexps); | ||||
|     const indicator = template.regexpProblemIndicator.cloneNode(true); | ||||
|     indicator.appendChild(document.createTextNode(entry.sectionsSkipped || '!')); | ||||
|     indicator.onclick = handleEvent.indicator; | ||||
|     $('.main-controls', entry).appendChild(indicator); | ||||
|   } | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -131,7 +131,7 @@ window.addEventListener('showStyles:done', function _() { | |||
|       if (result) { | ||||
|         result.installed = false; | ||||
|         result.installedStyleId = -1; | ||||
|         BG.clearTimeout(result.pingbackTimer); | ||||
|         (BG || window).clearTimeout(result.pingbackTimer); | ||||
|         renderActionButtons($('#' + RESULT_ID_PREFIX + result.id)); | ||||
|       } | ||||
|     }); | ||||
|  | @ -280,7 +280,7 @@ window.addEventListener('showStyles:done', function _() { | |||
|       return; | ||||
|     } | ||||
|     const md5Url = UPDATE_URL.replace('%', result.id); | ||||
|     getStylesSafe({md5Url}).then(([installedStyle]) => { | ||||
|     API.getStyles({md5Url}).then(([installedStyle]) => { | ||||
|       if (installedStyle) { | ||||
|         totalResults = Math.max(0, totalResults - 1); | ||||
|       } else { | ||||
|  | @ -522,7 +522,7 @@ window.addEventListener('showStyles:done', function _() { | |||
|     event.stopPropagation(); | ||||
|     const entry = this.closest('.search-result'); | ||||
|     saveScrollPosition(entry); | ||||
|     deleteStyleSafe({id: entry._result.installedStyleId}) | ||||
|     API.deleteStyle({id: entry._result.installedStyleId}) | ||||
|       .then(restoreScrollPosition); | ||||
|   } | ||||
| 
 | ||||
|  | @ -550,11 +550,11 @@ window.addEventListener('showStyles:done', function _() { | |||
|       style.updateUrl += settings.length ? '?' : ''; | ||||
|       // show a 'style installed' tooltip in the manager
 | ||||
|       style.reason = 'install'; | ||||
|       return saveStyleSafe(style); | ||||
|       return API.saveStyle(style); | ||||
|     }) | ||||
|     .catch(reason => { | ||||
|       const usoId = result.id; | ||||
|       console.debug('install:saveStyleSafe(usoID:', usoId, ') => [ERROR]: ', reason); | ||||
|       console.debug('install:saveStyle(usoID:', usoId, ') => [ERROR]: ', reason); | ||||
|       error('Error while downloading usoID:' + usoId + '\nReason: ' + reason); | ||||
|     }) | ||||
|     .then(() => { | ||||
|  | @ -574,7 +574,8 @@ window.addEventListener('showStyles:done', function _() { | |||
|   } | ||||
| 
 | ||||
|   function pingback(result) { | ||||
|     result.pingbackTimer = BG.setTimeout(BG.download, PINGBACK_DELAY, | ||||
|     const wnd = BG || window; | ||||
|     result.pingbackTimer = wnd.setTimeout(wnd.download, PINGBACK_DELAY, | ||||
|       BASE_URL + '/styles/install/' + result.id + '?source=stylish-ch'); | ||||
|   } | ||||
| 
 | ||||
|  | @ -721,9 +722,10 @@ window.addEventListener('showStyles:done', function _() { | |||
| 
 | ||||
|   function readCache(id) { | ||||
|     const key = CACHE_PREFIX + id; | ||||
|     return BG.chromeLocal.getValue(key).then(item => { | ||||
|     return chromeLocal.getValue(key).then(item => { | ||||
|       if (!cacheItemExpired(item)) { | ||||
|         return tryJSONparse(BG.LZString.decompressFromUTF16(item.payload)); | ||||
|         return chromeLocal.loadLZStringScript().then(() => | ||||
|           tryJSONparse(LZString.decompressFromUTF16(item.payload))); | ||||
|       } else if (item) { | ||||
|         chrome.storage.local.remove(key); | ||||
|       } | ||||
|  | @ -741,10 +743,11 @@ window.addEventListener('showStyles:done', function _() { | |||
|       return data; | ||||
|     } else { | ||||
|       debounce(cleanupCache, CACHE_CLEANUP_THROTTLE); | ||||
|       return BG.chromeLocal.setValue(CACHE_PREFIX + data.id, { | ||||
|         payload: BG.LZString.compressToUTF16(JSON.stringify(data)), | ||||
|         date: Date.now(), | ||||
|       }).then(() => data); | ||||
|       return chromeLocal.loadLZStringScript().then(() => | ||||
|         chromeLocal.setValue(CACHE_PREFIX + data.id, { | ||||
|           payload: LZString.compressToUTF16(JSON.stringify(data)), | ||||
|           date: Date.now(), | ||||
|         })).then(() => data); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	Block a user