diff --git a/_locales/en/messages.json b/_locales/en/messages.json index e54155ec..c9fb1aa6 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1530,6 +1530,14 @@ "message": "The value cannot be saved. Try reducing the amount of text.", "description": "Displayed when trying to save an excessively big value via storage.sync API" }, + "syncError": { + "message": "Sync failed", + "description": "Tooltip for the toolbar icon" + }, + "syncErrorRelogin": { + "message": "Sync failed.\nTry to re-login in Stylus options:\nclick 'disconnect' first, then 'connect'.", + "description": "Tooltip for the toolbar icon" + }, "toggleStyle": { "message": "Toggle style", "description": "Label for the checkbox to enable/disable a style" diff --git a/background/icon-manager.js b/background/icon-manager.js index 5d51e8e7..5e13a15c 100644 --- a/background/icon-manager.js +++ b/background/icon-manager.js @@ -5,10 +5,12 @@ /* global CHROME FIREFOX VIVALDI debounce ignoreChromeError */// toolbox.js 'use strict'; -(() => { +/* exported iconMan */ +const iconMan = (() => { const ICON_SIZES = FIREFOX || CHROME >= 55 && !VIVALDI ? [16, 32] : [19, 38]; const staleBadges = new Set(); const imageDataCache = new Map(); + const badgeOvr = {color: '', text: ''}; // https://github.com/openstyles/stylus/issues/335 let hasCanvas = loadImage(`/images/icon/${ICON_SIZES[0]}.png`) .then(({data}) => (hasCanvas = data.some(b => b !== 255))); @@ -54,6 +56,27 @@ ], () => debounce(refreshAllIcons), {runNow: true}); }); + return { + /** Calling with no params clears the override */ + overrideBadge({text = '', color = '', title = ''} = {}) { + if (badgeOvr.text === text) { + return; + } + badgeOvr.text = text; + badgeOvr.color = color; + refreshIconBadgeColor(); + setBadgeText({text}); + for (const tabId of tabMan.list()) { + if (badgeOvr) { + setBadgeText({tabId, text}); + } else { + refreshIconBadgeText(tabId); + } + } + chrome.browserAction.setTitle({title}); + }, + }; + function onPortDisconnected({sender}) { if (tabMan.get(sender.tab.id, 'styleIds')) { API.updateIconBadge.call({sender}, [], {lazyBadge: true}); @@ -61,6 +84,7 @@ } function refreshIconBadgeText(tabId) { + if (badgeOvr.text) return; const text = prefs.get('show-badge') ? `${getStyleCount(tabId)}` : ''; setBadgeText({tabId, text}); } @@ -133,9 +157,9 @@ } function refreshIconBadgeColor() { - const color = prefs.get(prefs.get('disableAll') ? 'badgeDisabled' : 'badgeNormal'); setBadgeBackgroundColor({ - color, + color: badgeOvr.color || + prefs.get(prefs.get('disableAll') ? 'badgeDisabled' : 'badgeNormal'), }); } diff --git a/background/sync-manager.js b/background/sync-manager.js index 0d5938f5..db816a1e 100644 --- a/background/sync-manager.js +++ b/background/sync-manager.js @@ -1,6 +1,7 @@ /* global API msg */// msg.js /* global chromeLocal */// storage-util.js /* global compareRevision */// common.js +/* global iconMan */ /* global prefs */ /* global tokenMan */ 'use strict'; @@ -26,6 +27,7 @@ const syncMan = (() => { errorMessage: null, login: false, }; + let lastError = null; let ctrl; let currentDrive; /** @type {Promise|boolean} will be `true` to avoid wasting a microtask tick on each `await` */ @@ -99,8 +101,10 @@ const syncMan = (() => { } await syncMan.syncNow(); status.errorMessage = null; + lastError = null; } catch (err) { status.errorMessage = err.message; + lastError = err; // FIXME: should we move this logic to options.js? if (!fromPref) { console.error(err); @@ -138,8 +142,10 @@ const syncMan = (() => { try { await (ctrl.isInit() ? ctrl.syncNow() : ctrl.start()).catch(handle401Error); status.errorMessage = null; + lastError = null; } catch (err) { status.errorMessage = err.message; + lastError = err; } emitStatusChange(); }, @@ -187,14 +193,14 @@ const syncMan = (() => { } async function handle401Error(err) { - let emit; + let authError = false; if (err.code === 401) { await tokenMan.revokeToken(currentDrive.name).catch(console.error); - emit = true; + authError = true; } else if (/User interaction required|Requires user interaction/i.test(err.message)) { - emit = true; + authError = true; } - if (emit) { + if (authError) { status.login = false; emitStatusChange(); } @@ -203,6 +209,32 @@ const syncMan = (() => { function emitStatusChange() { msg.broadcastExtension({method: 'syncStatusUpdate', status}); + + if (status.state !== STATES.connected || !lastError || isNetworkError(lastError)) { + iconMan.overrideBadge({}); + } else if (isGrantError(lastError)) { + iconMan.overrideBadge({ + text: 'x', + color: '#F00', + title: chrome.i18n.getMessage('syncErrorRelogin'), + }); + } else { + iconMan.overrideBadge({ + text: 'x', + color: '#F00', + title: chrome.i18n.getMessage('syncError'), + }); + } + } + + function isNetworkError(err) { + return err.name === 'TypeError' && /networkerror|failed to fetch/i.test(err.message); + } + + function isGrantError(err) { + if (err.code === 401) return true; + if (err.code === 400 && /invalid_grant/.test(err.message)) return true; + return false; } function getDrive(name) { diff --git a/background/tab-manager.js b/background/tab-manager.js index ca5b183d..0a1fea07 100644 --- a/background/tab-manager.js +++ b/background/tab-manager.js @@ -53,6 +53,7 @@ const tabMan = (() => { } }, + /** @returns {IterableIterator} */ list() { return cache.keys(); }, diff --git a/package-lock.json b/package-lock.json index 6abf6ec6..9fce00ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -645,7 +645,6 @@ "espree": "7.3.0", "esprima": "4.0.1", "fluent-syntax": "0.13.0", - "fsevents": "2.2.1", "glob": "7.1.6", "is-mergeable-object": "1.1.1", "jed": "1.1.1", @@ -1855,12 +1854,6 @@ "engines": [ "node >=0.10.0" ], - "dependencies": { - "dtrace-provider": "~0.8", - "moment": "^2.19.3", - "mv": "~2", - "safe-json-stringify": "~1" - }, "bin": { "bunyan": "bin/bunyan" }, @@ -2144,7 +2137,6 @@ "dependencies": { "anymatch": "~3.1.1", "braces": "~3.0.2", - "fsevents": "~2.1.2", "glob-parent": "~5.1.0", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", @@ -4559,9 +4551,6 @@ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", "dev": true, - "dependencies": { - "graceful-fs": "^4.1.6" - }, "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -6995,7 +6984,6 @@ "integrity": "sha512-ssHt0dkljEDaKmTgQ04DQgx2ag6G2gMPxA5hpcsoeTbfDgRf2fC2gNSRc6kISjD7ckCpHwwQvXxuTBK8402fXg==", "dev": true, "dependencies": { - "encoding": "^0.1.12", "minipass": "^3.1.0", "minipass-pipeline": "^1.2.2", "minipass-sized": "^1.0.3", @@ -11284,10 +11272,8 @@ "integrity": "sha512-aWAgTW4MoSJzZPAicljkO1hsi1oKj/RRq/OJQh2PKI2UKL04c2Bs+MBOB+BBABHTXJpf9mCwHN7ANCvYsvY2sg==", "dev": true, "dependencies": { - "chokidar": "^3.4.1", "graceful-fs": "^4.1.2", - "neo-async": "^2.5.0", - "watchpack-chokidar2": "^2.0.0" + "neo-async": "^2.5.0" }, "optionalDependencies": { "chokidar": "^3.4.1", @@ -11371,7 +11357,6 @@ "anymatch": "^2.0.0", "async-each": "^1.0.1", "braces": "^2.3.2", - "fsevents": "^1.2.7", "glob-parent": "^3.1.0", "inherits": "^2.0.3", "is-binary-path": "^1.0.0",