Embed options in manager (#828)

* Embed options in manager

* fix indent again

* Fix edit URL detected as manage URL when creating manager style from popup

* Syntax, hash only, and prevent empty hash

* Fix: move origin check to background

* Rename eslintrc

* Refactor: openURL

* Add: fixme comment about openEditor

* Fix: allow activating manager in other windows

* Add: trimHash method

* Fix: limit the scope of styleViaAPI

* Breaking: add router, keep search params

* Fix: focus options when activated

* Add: some fixme

* Fix: remove unused fixme

* Fix: minor

* Fix: remove unused message

* Add: doc

* Change: activate manager in other windows

* Fix: make sure sender is available in getTabUrlPrefix

* Add: openManage API

* Change: reuse editor in openEditor

* Fix: greedly pop the buffer

* Fix: backward detection

* Fix: remove unused important

* Fix: remove unused workaround

* Fix: avoid empty search param

* Change: detect all kinds of manager in openManage

* Fix: minor

* Manage button text

Co-authored-by: eight <eight04@gmail.com>
This commit is contained in:
narcolepticinsomniac 2020-02-01 23:36:54 -05:00 committed by GitHub
parent d3ee6d9498
commit 1f12d50aaf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 551 additions and 294 deletions

View File

@ -31,7 +31,7 @@ rules:
dot-location: [2, property] dot-location: [2, property]
dot-notation: [0] dot-notation: [0]
eol-last: [2] eol-last: [2]
eqeqeq: [1, always] eqeqeq: [1, smart]
func-call-spacing: [2, never] func-call-spacing: [2, never]
func-name-matching: [0] func-name-matching: [0]
func-names: [0] func-names: [0]
@ -84,7 +84,7 @@ rules:
no-empty-function: [0] no-empty-function: [0]
no-empty-pattern: [2] no-empty-pattern: [2]
no-empty: [2, {allowEmptyCatch: true}] no-empty: [2, {allowEmptyCatch: true}]
no-eq-null: [2] no-eq-null: [0]
no-eval: [2] no-eval: [2]
no-ex-assign: [2] no-ex-assign: [2]
no-extend-native: [2] no-extend-native: [2]

View File

@ -398,11 +398,7 @@
"message": "Управление", "message": "Управление",
"description": "Link to open the manage page." "description": "Link to open the manage page."
}, },
"openOptionsManage": { "openOptions": {
"message": "Прозорец за настройките",
"description": "Go to Options UI"
},
"openOptionsPopup": {
"message": "Настройки", "message": "Настройки",
"description": "Go to Options UI" "description": "Go to Options UI"
}, },

View File

@ -723,11 +723,7 @@
"message": "Spravovat", "message": "Spravovat",
"description": "Link to open the manage page." "description": "Link to open the manage page."
}, },
"openOptionsManage": { "openOptions": {
"message": "Možnosti rozhraní",
"description": "Go to Options UI"
},
"openOptionsPopup": {
"message": "Možnosti", "message": "Možnosti",
"description": "Go to Options UI" "description": "Go to Options UI"
}, },

View File

@ -897,11 +897,7 @@
"message": "Verwalten", "message": "Verwalten",
"description": "Link to open the manage page." "description": "Link to open the manage page."
}, },
"openOptionsManage": { "openOptions": {
"message": "Optionen",
"description": "Go to Options UI"
},
"openOptionsPopup": {
"message": "Optionen", "message": "Optionen",
"description": "Go to Options UI" "description": "Go to Options UI"
}, },

View File

@ -927,11 +927,7 @@
"message": "Manage", "message": "Manage",
"description": "Link to open the manage page." "description": "Link to open the manage page."
}, },
"openOptionsManage": { "openOptions": {
"message": "Options UI",
"description": "Go to Options UI"
},
"openOptionsPopup": {
"message": "Options", "message": "Options",
"description": "Go to Options UI" "description": "Go to Options UI"
}, },

View File

@ -897,11 +897,7 @@
"message": "Administrar", "message": "Administrar",
"description": "Link to open the manage page." "description": "Link to open the manage page."
}, },
"openOptionsManage": { "openOptions": {
"message": "Interfaz de opciones",
"description": "Go to Options UI"
},
"openOptionsPopup": {
"message": "Opciones", "message": "Opciones",
"description": "Go to Options UI" "description": "Go to Options UI"
}, },

View File

@ -815,11 +815,7 @@
"message": "Halda", "message": "Halda",
"description": "Link to open the manage page." "description": "Link to open the manage page."
}, },
"openOptionsManage": { "openOptions": {
"message": "Valikute liides",
"description": "Go to Options UI"
},
"openOptionsPopup": {
"message": "Valikud", "message": "Valikud",
"description": "Go to Options UI" "description": "Go to Options UI"
}, },

View File

@ -913,11 +913,7 @@
"message": "Gestion", "message": "Gestion",
"description": "Link to open the manage page." "description": "Link to open the manage page."
}, },
"openOptionsManage": { "openOptions": {
"message": "Paramètres d'interface graphique",
"description": "Go to Options UI"
},
"openOptionsPopup": {
"message": "Paramètres", "message": "Paramètres",
"description": "Go to Options UI" "description": "Go to Options UI"
}, },

View File

@ -591,11 +591,7 @@
"message": "ניהול", "message": "ניהול",
"description": "Link to open the manage page." "description": "Link to open the manage page."
}, },
"openOptionsManage": { "openOptions": {
"message": "אפשרויות UI",
"description": "Go to Options UI"
},
"openOptionsPopup": {
"message": "אפשרויות", "message": "אפשרויות",
"description": "Go to Options UI" "description": "Go to Options UI"
}, },

View File

@ -677,11 +677,7 @@
"message": "Kezelés", "message": "Kezelés",
"description": "Link to open the manage page." "description": "Link to open the manage page."
}, },
"openOptionsManage": { "openOptions": {
"message": "A beállítások felülete",
"description": "Go to Options UI"
},
"openOptionsPopup": {
"message": "Beállítások", "message": "Beállítások",
"description": "Go to Options UI" "description": "Go to Options UI"
}, },

View File

@ -639,11 +639,7 @@
"message": "Gestisci gli stili installati", "message": "Gestisci gli stili installati",
"description": "Link to open the manage page." "description": "Link to open the manage page."
}, },
"openOptionsManage": { "openOptions": {
"message": "Opzioni UI",
"description": "Go to Options UI"
},
"openOptionsPopup": {
"message": "Opzioni", "message": "Opzioni",
"description": "Go to Options UI" "description": "Go to Options UI"
}, },

View File

@ -913,11 +913,7 @@
"message": "管理", "message": "管理",
"description": "Link to open the manage page." "description": "Link to open the manage page."
}, },
"openOptionsManage": { "openOptions": {
"message": "オプション UI",
"description": "Go to Options UI"
},
"openOptionsPopup": {
"message": "オプション", "message": "オプション",
"description": "Go to Options UI" "description": "Go to Options UI"
}, },

View File

@ -901,11 +901,7 @@
"message": "Beheren", "message": "Beheren",
"description": "Link to open the manage page." "description": "Link to open the manage page."
}, },
"openOptionsManage": { "openOptions": {
"message": "Opties",
"description": "Go to Options UI"
},
"openOptionsPopup": {
"message": "Opties", "message": "Opties",
"description": "Go to Options UI" "description": "Go to Options UI"
}, },

View File

@ -917,11 +917,7 @@
"message": "Zarządzaj", "message": "Zarządzaj",
"description": "Link to open the manage page." "description": "Link to open the manage page."
}, },
"openOptionsManage": { "openOptions": {
"message": "Opcje interfejsu",
"description": "Go to Options UI"
},
"openOptionsPopup": {
"message": "Opcje", "message": "Opcje",
"description": "Go to Options UI" "description": "Go to Options UI"
}, },

View File

@ -241,11 +241,7 @@
"message": "Nenhum estilo instalado para este site.", "message": "Nenhum estilo instalado para este site.",
"description": "Text displayed when no styles are installed for the current site" "description": "Text displayed when no styles are installed for the current site"
}, },
"openManage": { "openOptions": {
"message": "Gerenciar estilos instalados",
"description": "Link to open the manage page."
},
"openOptionsPopup": {
"message": "Opções", "message": "Opções",
"description": "Go to Options UI" "description": "Go to Options UI"
}, },

View File

@ -673,11 +673,7 @@
"message": "Gerir", "message": "Gerir",
"description": "Link to open the manage page." "description": "Link to open the manage page."
}, },
"openOptionsManage": { "openOptions": {
"message": "interface de Opções",
"description": "Go to Options UI"
},
"openOptionsPopup": {
"message": "Opções", "message": "Opções",
"description": "Go to Options UI" "description": "Go to Options UI"
}, },

View File

@ -617,11 +617,7 @@
"message": "Managerul", "message": "Managerul",
"description": "Link to open the manage page." "description": "Link to open the manage page."
}, },
"openOptionsManage": { "openOptions": {
"message": "UI cu opțiuni",
"description": "Go to Options UI"
},
"openOptionsPopup": {
"message": "Opțiuni", "message": "Opțiuni",
"description": "Go to Options UI" "description": "Go to Options UI"
}, },

View File

@ -921,11 +921,7 @@
"message": "Менеджер", "message": "Менеджер",
"description": "Link to open the manage page." "description": "Link to open the manage page."
}, },
"openOptionsManage": { "openOptions": {
"message": "Настройки",
"description": "Go to Options UI"
},
"openOptionsPopup": {
"message": "Настройки", "message": "Настройки",
"description": "Go to Options UI" "description": "Go to Options UI"
}, },

View File

@ -871,11 +871,7 @@
"message": "Hantera installerade stilar", "message": "Hantera installerade stilar",
"description": "Link to open the manage page." "description": "Link to open the manage page."
}, },
"openOptionsManage": { "openOptions": {
"message": "Alternativ UI",
"description": "Go to Options UI"
},
"openOptionsPopup": {
"message": "Alternativ", "message": "Alternativ",
"description": "Go to Options UI" "description": "Go to Options UI"
}, },

View File

@ -917,11 +917,7 @@
"message": "管理样式", "message": "管理样式",
"description": "Link to open the manage page." "description": "Link to open the manage page."
}, },
"openOptionsManage": { "openOptions": {
"message": "设置用户界面",
"description": "Go to Options UI"
},
"openOptionsPopup": {
"message": "设置用户界面", "message": "设置用户界面",
"description": "Go to Options UI" "description": "Go to Options UI"
}, },

View File

@ -917,11 +917,7 @@
"message": "管理已安裝樣式", "message": "管理已安裝樣式",
"description": "Link to open the manage page." "description": "Link to open the manage page."
}, },
"openOptionsManage": { "openOptions": {
"message": "選項介面",
"description": "Go to Options UI"
},
"openOptionsPopup": {
"message": "選項", "message": "選項",
"description": "Go to Options UI" "description": "Go to Options UI"
}, },

View File

@ -1,6 +1,8 @@
/* global download prefs openURL FIREFOX CHROME VIVALDI /* global download prefs openURL FIREFOX CHROME VIVALDI
debounce URLS ignoreChromeError getTab debounce URLS ignoreChromeError getTab
styleManager msg navigatorUtil iconUtil workerUtil contentScripts sync */ styleManager msg navigatorUtil iconUtil workerUtil contentScripts sync
findExistTab createTab activateTab isTabReplaceable getActiveTab */
'use strict'; 'use strict';
// eslint-disable-next-line no-var // eslint-disable-next-line no-var
@ -28,7 +30,11 @@ window.API_METHODS = Object.assign(window.API_METHODS || {}, {
removeExclusion: styleManager.removeExclusion, removeExclusion: styleManager.removeExclusion,
getTabUrlPrefix() { getTabUrlPrefix() {
return this.sender.tab.url.match(/^([\w-]+:\/+[^/#]+)/)[1]; const {url} = this.sender.tab;
if (url.startsWith(URLS.ownOrigin)) {
return 'stylus';
}
return url.match(/^([\w-]+:\/+[^/#]+)/)[1];
}, },
download(msg) { download(msg) {
@ -69,7 +75,9 @@ window.API_METHODS = Object.assign(window.API_METHODS || {}, {
syncStop: sync.stop, syncStop: sync.stop,
syncNow: sync.syncNow, syncNow: sync.syncNow,
getSyncStatus: sync.getStatus, getSyncStatus: sync.getStatus,
syncLogin: sync.login syncLogin: sync.login,
openManage
}); });
// eslint-disable-next-line no-var // eslint-disable-next-line no-var
@ -174,9 +182,8 @@ chrome.runtime.onInstalled.addListener(({reason}) => {
// ************************************************************************* // *************************************************************************
// browser commands // browser commands
browserCommands = { browserCommands = {
openManage() { openManage,
openURL({url: 'manage.html'}); openOptions: () => openManage({options: true}),
},
styleDisableAll(info) { styleDisableAll(info) {
prefs.set('disableAll', info ? info.checked : !prefs.get('disableAll')); prefs.set('disableAll', info ? info.checked : !prefs.get('disableAll'));
}, },
@ -197,6 +204,10 @@ contextMenus = {
title: 'openStylesManager', title: 'openStylesManager',
click: browserCommands.openManage, click: browserCommands.openManage,
}, },
'open-options': {
title: 'openOptions',
click: browserCommands.openOptions,
},
'editor.contextDelete': { 'editor.contextDelete': {
presentIf: () => !FIREFOX && prefs.get('editor.contextDelete'), presentIf: () => !FIREFOX && prefs.get('editor.contextDelete'),
title: 'editDeleteText', title: 'editDeleteText',
@ -388,15 +399,55 @@ function onRuntimeMessage(msg, sender) {
return fn.apply(context, msg.args); return fn.apply(context, msg.args);
} }
// FIXME: popup.js also open editor but it doesn't use this API. function openEditor(params) {
function openEditor({id}) { /* Open the editor. Activate if it is already opened
let url = '/edit.html';
if (id) { params: {
url += `?id=${id}`; id?: Number,
domain?: String,
'url-prefix'?: String
} }
if (chrome.windows && prefs.get('openEditInWindow')) { */
chrome.windows.create(Object.assign({url}, prefs.get('windowPosition'))); const searchParams = new URLSearchParams();
} else { for (const key in params) {
openURL({url}); searchParams.set(key, params[key]);
} }
const search = searchParams.toString();
return openURL({
url: 'edit.html' + (search && `?${search}`),
newWindow: prefs.get('openEditInWindow'),
windowPosition: prefs.get('windowPosition'),
currentWindow: null
});
}
function openManage({options = false, search} = {}) {
let url = chrome.runtime.getURL('manage.html');
if (search) {
url += `?search=${encodeURIComponent(search)}`;
}
if (options) {
url += '#stylus-options';
}
return findExistTab({
url,
currentWindow: null,
ignoreHash: true,
ignoreSearch: true
})
.then(tab => {
if (tab) {
return Promise.all([
activateTab(tab),
tab.url !== url && msg.sendTab(tab.id, {method: 'pushState', url})
.catch(console.error)
]);
}
return getActiveTab().then(tab => {
if (isTabReplaceable(tab, url)) {
return activateTab(tab, {url});
}
return createTab({url});
});
});
} }

View File

@ -53,7 +53,7 @@ const contentScripts = (() => {
} }
function injectToAllTabs() { function injectToAllTabs() {
return queryTabs().then(tabs => { return queryTabs({}).then(tabs => {
for (const tab of tabs) { for (const tab of tabs) {
// skip lazy-loaded aka unloaded tabs that seem to start loading on message in FF // skip lazy-loaded aka unloaded tabs that seem to start loading on message in FF
if (tab.width) { if (tab.width) {

View File

@ -202,18 +202,20 @@ const APPLY = (() => {
} }
function applyOnMessage(request) { function applyOnMessage(request) {
if (request.method === 'ping') {
return true;
}
if (STYLE_VIA_API) { if (STYLE_VIA_API) {
if (request.method === 'urlChanged') { if (request.method === 'urlChanged') {
request.method = 'styleReplaceAll'; request.method = 'styleReplaceAll';
} }
if (/^(style|updateCount)/.test(request.method)) {
API.styleViaAPI(request); API.styleViaAPI(request);
return; return;
} }
}
switch (request.method) { switch (request.method) {
case 'ping':
return true;
case 'styleDeleted': case 'styleDeleted':
styleInjector.remove(request.style.id); styleInjector.remove(request.style.id);
break; break;
@ -273,7 +275,11 @@ const APPLY = (() => {
if (parentDomain) { if (parentDomain) {
return Promise.resolve(); return Promise.resolve();
} }
return API.getTabUrlPrefix() return msg.send({
method: 'invokeAPI',
name: 'getTabUrlPrefix',
args: []
})
.then(newDomain => { .then(newDomain => {
parentDomain = newDomain; parentDomain = newDomain;
}); });

View File

@ -66,7 +66,7 @@ const regExpTester = (() => {
return rxData; return rxData;
}); });
const getMatchInfo = m => m && {text: m[0], pos: m.index}; const getMatchInfo = m => m && {text: m[0], pos: m.index};
queryTabs().then(tabs => { queryTabs({}).then(tabs => {
const supported = tabs.map(tab => tab.url) const supported = tabs.map(tab => tab.url)
.filter(url => URLS.supported(url)); .filter(url => URLS.supported(url));
const unique = [...new Set(supported).values()]; const unique = [...new Set(supported).values()];

View File

@ -1,6 +1,7 @@
/* exported getActiveTab onTabReady stringAsRegExp getTabRealURL openURL /* exported getActiveTab onTabReady stringAsRegExp getTabRealURL openURL
getStyleWithNoCode tryRegExp sessionStorageHash download deepEqual getStyleWithNoCode tryRegExp sessionStorageHash download deepEqual
closeCurrentTab capitalize */ closeCurrentTab capitalize CHROME_HAS_BORDER_BUG */
/* global promisify */
'use strict'; 'use strict';
const CHROME = Boolean(chrome.app) && parseInt(navigator.userAgent.match(/Chrom\w+\/(?:\d+\.){2}(\d+)|$/)[1]); const CHROME = Boolean(chrome.app) && parseInt(navigator.userAgent.match(/Chrom\w+\/(?:\d+\.){2}(\d+)|$/)[1]);
@ -28,6 +29,7 @@ if (!CHROME && !chrome.browserAction.openPopup) {
const URLS = { const URLS = {
ownOrigin: chrome.runtime.getURL(''), ownOrigin: chrome.runtime.getURL(''),
// FIXME delete?
optionsUI: [ optionsUI: [
chrome.runtime.getURL('options.html'), chrome.runtime.getURL('options.html'),
'chrome://extensions/?options=' + chrome.runtime.id, 'chrome://extensions/?options=' + chrome.runtime.id,
@ -91,12 +93,13 @@ if (IS_BG) {
// Object.defineProperty(window, 'localStorage', {value: {}}); // Object.defineProperty(window, 'localStorage', {value: {}});
// Object.defineProperty(window, 'sessionStorage', {value: {}}); // Object.defineProperty(window, 'sessionStorage', {value: {}});
function queryTabs(options = {}) { const createTab = promisify(chrome.tabs.create.bind(chrome.tabs));
return new Promise(resolve => const queryTabs = promisify(chrome.tabs.query.bind(chrome.tabs));
chrome.tabs.query(options, tabs => const updateTab = promisify(chrome.tabs.update.bind(chrome.tabs));
resolve(tabs))); const moveTabs = promisify(chrome.tabs.move.bind(chrome.tabs));
} // FIXME: is it possible that chrome.windows is undefined?
const updateWindow = promisify(chrome.windows.update.bind(chrome.windows));
const createWindow = promisify(chrome.windows.create.bind(chrome.windows));
function getTab(id) { function getTab(id) {
return new Promise(resolve => return new Promise(resolve =>
@ -192,6 +195,39 @@ function onTabReady(tabOrId) {
}); });
} }
function urlToMatchPattern(url, ignoreSearch) {
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Match_patterns
if (!/^(http|https|ws|wss|ftp|data|file)$/.test(url.protocol)) {
return undefined;
}
if (ignoreSearch) {
return [
`${url.protocol}//${url.hostname}/${url.pathname}`,
`${url.protocol}//${url.hostname}/${url.pathname}?*`
];
}
// FIXME: is %2f allowed in pathname and search?
return `${url.protocol}//${url.hostname}/${url.pathname}${url.search}`;
}
function findExistTab({url, currentWindow, ignoreHash = true, ignoreSearch = false}) {
url = new URL(url);
return queryTabs({url: urlToMatchPattern(url, ignoreSearch), currentWindow})
// FIXME: is tab.url always normalized?
.then(tabs => tabs.find(matchTab));
function matchTab(tab) {
const tabUrl = new URL(tab.url);
return tabUrl.protocol === url.protocol &&
tabUrl.username === url.username &&
tabUrl.password === url.password &&
tabUrl.hostname === url.hostname &&
tabUrl.port === url.port &&
tabUrl.pathname === url.pathname &&
(ignoreSearch || tabUrl.search === url.search) &&
(ignoreHash || tabUrl.hash === url.hash);
}
}
/** /**
* Opens a tab or activates an existing one, * Opens a tab or activates an existing one,
@ -211,72 +247,77 @@ function onTabReady(tabOrId) {
* JSONifiable data to be sent to the tab via sendMessage() * JSONifiable data to be sent to the tab via sendMessage()
* @returns {Promise<Tab>} Promise that resolves to the opened/activated tab * @returns {Promise<Tab>} Promise that resolves to the opened/activated tab
*/ */
function openURL({ function openURL(options) {
// https://github.com/eslint/eslint/issues/10639 if (typeof options === 'string') {
// eslint-disable-next-line no-undef options = {url: options};
url = arguments[0], }
let {
url,
index, index,
active, active,
currentWindow = true, currentWindow = true,
}) { newWindow = false,
url = url.includes('://') ? url : chrome.runtime.getURL(url); windowPosition
// [some] chromium forks don't handle their fake branded protocols } = options;
url = url.replace(/^(opera|vivaldi)/, 'chrome');
// FF doesn't handle moz-extension:// URLs (bug)
// FF decodes %2F in encoded parameters (bug)
// API doesn't handle the hash-fragment part
const urlQuery =
url.startsWith('moz-extension') ||
url.startsWith('chrome:') ?
undefined :
FIREFOX && url.includes('%2F') ?
url.replace(/%2F.*/, '*').replace(/#.*/, '') :
url.replace(/#.*/, '');
return queryTabs({url: urlQuery, currentWindow}).then(maybeSwitch); if (!url.includes('://')) {
url = chrome.runtime.getURL(url);
function maybeSwitch(tabs = []) {
const urlWithSlash = url + '/';
const urlFF = FIREFOX && url.replace(/%2F/g, '/');
const tab = tabs.find(({url: u}) => u === url || u === urlFF || u === urlWithSlash);
if (!tab) {
return getActiveTab().then(maybeReplace);
} }
if (index !== undefined && tab.index !== index) { return findExistTab({url, currentWindow}).then(tab => {
chrome.tabs.move(tab.id, {index}); if (tab) {
// update url if only hash is different?
// we can't update URL if !url.includes('#') since it refreshes the page
// FIXME: should we move the tab (i.e. specifying index)?
if (tab.url !== url && tab.url.split('#')[0] === url.split('#')[0] &&
url.includes('#')) {
return activateTab(tab, {url, index});
} }
return activateTab(tab); return activateTab(tab, {index});
} }
if (newWindow) {
// update current NTP or about:blank return createWindow(Object.assign({url}, windowPosition));
// except when 'url' is chrome:// or chrome-extension:// in incognito }
function maybeReplace(tab) { return getActiveTab().then(tab => {
const chromeInIncognito = tab && tab.incognito && url.startsWith('chrome'); if (isTabReplaceable(tab, url)) {
const emptyTab = tab && URLS.emptyTab.includes(tab.url); // don't move the tab in this case
if (emptyTab && !chromeInIncognito) { return activateTab(tab, {url});
return new Promise(resolve =>
chrome.tabs.update({url}, resolve));
} }
const options = {url, index, active}; const options = {url, index, active};
// FF57+ supports openerTabId, but not in Android (indicated by the absence of chrome.windows) // FF57+ supports openerTabId, but not in Android (indicated by the absence of chrome.windows)
if (tab && (!FIREFOX || FIREFOX >= 57 && chrome.windows) && !chromeInIncognito) { // FIXME: is it safe to assume that the current tab is the opener?
if (tab && !tab.incognito && (!FIREFOX || FIREFOX >= 57 && chrome.windows)) {
options.openerTabId = tab.id; options.openerTabId = tab.id;
} }
return new Promise(resolve => return createTab(options);
chrome.tabs.create(options, resolve)); });
} });
} }
// replace empty tab (NTP or about:blank)
// except when new URL is chrome:// or chrome-extension:// and the empty tab is
// in incognito
function isTabReplaceable(tab, newUrl) {
if (!tab || !URLS.emptyTab.includes(tab.url)) {
return false;
}
// FIXME: but why?
if (tab.incognito && newUrl.startsWith('chrome')) {
return false;
}
return true;
}
function activateTab(tab) { function activateTab(tab, {url, index} = {}) {
const options = {active: true};
if (url) {
options.url = url;
}
return Promise.all([ return Promise.all([
new Promise(resolve => { updateTab(tab.id, options),
chrome.tabs.update(tab.id, {active: true}, resolve); updateWindow(tab.windowId, {focused: true}),
}), index != null && moveTabs(tab.id, {index})
chrome.windows && new Promise(resolve => { ])
chrome.windows.update(tab.windowId, {focused: true}, resolve); .then(() => tab);
}),
]).then(([tab]) => tab);
} }

99
js/router.js Normal file
View File

@ -0,0 +1,99 @@
/* global deepEqual msg */
/* exported router */
'use strict';
const router = (() => {
const buffer = [];
const watchers = [];
document.addEventListener('DOMContentLoaded', () => update());
window.addEventListener('popstate', () => update());
window.addEventListener('hashchange', () => update());
msg.on(e => {
if (e.method === 'pushState' && e.url !== location.href) {
history.pushState(history.state, null, e.url);
update();
return true;
}
});
return {watch, updateSearch, getSearch, updateHash};
function watch(options, callback) {
/* Watch search params or hash and get notified on change.
options: {search?: Array<key: String>, hash?: String}
callback: (Array<value: String | null> | Boolean) => void
`hash` should always start with '#'.
When watching search params, callback receives a list of values.
When watching hash, callback receives a boolean.
*/
watchers.push({options, callback});
}
function updateSearch(key, value) {
const search = new URLSearchParams(location.search.replace(/^\?/, ''));
if (!value) {
search.delete(key);
} else {
search.set(key, value);
}
const finalSearch = search.toString();
if (finalSearch) {
history.replaceState(history.state, null, `?${finalSearch}${location.hash}`);
} else {
history.replaceState(history.state, null, `${location.pathname}${location.hash}`);
}
update(true);
}
function updateHash(hash) {
/* hash: String
Send an empty string to remove the hash.
*/
if (buffer.length > 1) {
if (!hash && !buffer[buffer.length - 2].includes('#') ||
hash && buffer[buffer.length - 2].endsWith(hash)) {
history.back();
return;
}
}
if (!hash) {
hash = ' ';
}
history.pushState(history.state, null, hash);
update();
}
function getSearch(key) {
return new URLSearchParams(location.search.replace(/^\?/, '')).get(key);
}
function update(replace) {
if (!buffer.length) {
buffer.push(location.href);
} else if (buffer[buffer.length - 1] === location.href) {
return;
} else if (replace) {
buffer[buffer.length - 1] = location.href;
} else if (buffer.length > 1 && buffer[buffer.length - 2] === location.href) {
buffer.pop();
} else {
buffer.push(location.href);
}
for (const {options, callback} of watchers) {
let state;
if (options.hash) {
state = options.hash === location.hash;
} else if (options.search) {
// TODO: remove .replace(/^\?/, '') when minimum_chrome_version >= 52 (https://crbug.com/601425)
const search = new URLSearchParams(location.search.replace(/^\?/, ''));
state = options.search.map(key => search.get(key));
}
if (!deepEqual(state, options.currentState)) {
options.currentState = state;
callback(state);
}
}
}
})();

View File

@ -152,6 +152,7 @@
<script src="js/messaging.js"></script> <script src="js/messaging.js"></script>
<script src="js/prefs.js"></script> <script src="js/prefs.js"></script>
<script src="js/msg.js"></script> <script src="js/msg.js"></script>
<script src="js/router.js"></script>
<script src="content/style-injector.js"></script> <script src="content/style-injector.js"></script>
<script src="content/apply.js"></script> <script src="content/apply.js"></script>
<script src="js/localization.js"></script> <script src="js/localization.js"></script>
@ -358,7 +359,7 @@
</div> </div>
<div id="options-buttons"> <div id="options-buttons">
<button id="manage-options-button" i18n-text="openOptionsManage"></button> <button id="manage-options-button" i18n-text="openOptions"></button>
<button id="manage-shortcuts-button" class="chromium-only" <button id="manage-shortcuts-button" class="chromium-only"
i18n-text="shortcuts" i18n-text="shortcuts"
i18n-title="shortcutsNote"></button> i18n-title="shortcutsNote"></button>

View File

@ -1,4 +1,4 @@
/* global installed messageBox sorter $ $$ $create t debounce prefs API onDOMready */ /* global installed messageBox sorter $ $$ $create t debounce prefs API router */
/* exported filterAndAppend */ /* exported filterAndAppend */
'use strict'; 'use strict';
@ -9,11 +9,17 @@ const filtersSelector = {
numTotal: 0, numTotal: 0,
}; };
// TODO: remove .replace(/^\?/, '') when minimum_chrome_version >= 52 (https://crbug.com/601425) let initialized = false;
const urlFilterParam = new URLSearchParams(location.search.replace(/^\?/, '')).get('url');
if (location.search) { router.watch({search: ['search']}, ([search]) => {
history.replaceState(0, document.title, location.origin + location.pathname); $('#search').value = search || '';
} if (!initialized) {
init();
initialized = true;
} else {
searchStyles();
}
});
HTMLSelectElement.prototype.adjustWidth = function () { HTMLSelectElement.prototype.adjustWidth = function () {
const option0 = this.selectedOptions[0]; const option0 = this.selectedOptions[0];
@ -30,11 +36,11 @@ HTMLSelectElement.prototype.adjustWidth = function () {
parent.replaceChild(this, singleSelect); parent.replaceChild(this, singleSelect);
}; };
onDOMready().then(() => { function init() {
$('#search').oninput = searchStyles; $('#search').oninput = e => {
if (urlFilterParam) { router.updateSearch('search', e.target.value);
$('#search').value = 'url:' + urlFilterParam; };
}
$('#search-help').onclick = event => { $('#search-help').onclick = event => {
event.preventDefault(); event.preventDefault();
messageBox({ messageBox({
@ -120,6 +126,7 @@ onDOMready().then(() => {
} }
} }
filterOnChange({forceRefilter: true}); filterOnChange({forceRefilter: true});
router.updateSearch('search', '');
}; };
// Adjust width after selects are visible // Adjust width after selects are visible
@ -131,7 +138,7 @@ onDOMready().then(() => {
}); });
filterOnChange({forceRefilter: true}); filterOnChange({forceRefilter: true});
}); }
function filterOnChange({target: el, forceRefilter}) { function filterOnChange({target: el, forceRefilter}) {
@ -271,7 +278,7 @@ function showFiltersStats() {
} }
function searchStyles({immediately, container}) { function searchStyles({immediately, container} = {}) {
const el = $('#search'); const el = $('#search');
const query = el.value.trim(); const query = el.value.trim();
if (query === el.lastValue && !immediately && !container) { if (query === el.lastValue && !immediately && !container) {

View File

@ -7,8 +7,12 @@ const STYLISH_DUMP_FILE_EXT = '.txt';
const STYLUS_BACKUP_FILE_EXT = '.json'; const STYLUS_BACKUP_FILE_EXT = '.json';
onDOMready().then(() => { onDOMready().then(() => {
$('#file-all-styles').onclick = exportToFile; $('#file-all-styles').onclick = event => {
$('#unfile-all-styles').onclick = () => { event.preventDefault();
exportToFile();
};
$('#unfile-all-styles').onclick = event => {
event.preventDefault();
importFromFile({fileTypeFilter: STYLUS_BACKUP_FILE_EXT}); importFromFile({fileTypeFilter: STYLUS_BACKUP_FILE_EXT});
}; };

View File

@ -1109,6 +1109,22 @@ input[id^="manage.newUI"] {
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
#stylus-embedded-options {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100vh;
border: 0;
z-index: 2147483647;
background-color: hsla(0, 0%, 0%, .45);
animation: fadein .25s ease-in-out;
}
#stylus-embedded-options.fadeout {
animation: fadeout .25s ease-in-out;
}
@keyframes fadein { @keyframes fadein {
from { from {
opacity: 0; opacity: 0;

View File

@ -1,13 +1,13 @@
/* /*
global messageBox getStyleWithNoCode global messageBox getStyleWithNoCode
filterAndAppend urlFilterParam showFiltersStats filterAndAppend showFiltersStats
checkUpdate handleUpdateInstalled checkUpdate handleUpdateInstalled
objectDiff objectDiff
configDialog configDialog
sorter msg prefs API onDOMready $ $$ $create template setupLivePrefs sorter msg prefs API onDOMready $ $$ $create template setupLivePrefs
URLS enforceInputRange t tWordBreak formatDate URLS enforceInputRange t tWordBreak formatDate
getOwnTab getActiveTab openURL animateElement sessionStorageHash debounce getOwnTab getActiveTab openURL animateElement sessionStorageHash debounce
scrollElementIntoView CHROME VIVALDI FIREFOX scrollElementIntoView CHROME VIVALDI FIREFOX router
*/ */
'use strict'; 'use strict';
@ -35,7 +35,8 @@ const handleEvent = {};
Promise.all([ Promise.all([
API.getAllStyles(true), API.getAllStyles(true),
urlFilterParam && API.searchDB({query: 'url:' + urlFilterParam}), // FIXME: integrate this into filter.js
router.getSearch('search') && API.searchDB({query: router.getSearch('search')}),
Promise.all([ Promise.all([
onDOMready(), onDOMready(),
prefs.initializing, prefs.initializing,
@ -80,7 +81,9 @@ function onRuntimeMessage(msg) {
function initGlobalEvents() { function initGlobalEvents() {
installed = $('#installed'); installed = $('#installed');
installed.onclick = handleEvent.entryClicked; installed.onclick = handleEvent.entryClicked;
$('#manage-options-button').onclick = () => chrome.runtime.openOptionsPage(); $('#manage-options-button').onclick = () => {
router.updateHash('#stylus-options');
};
{ {
const btn = $('#manage-shortcuts-button'); const btn = $('#manage-shortcuts-button');
btn.onclick = btn.onclick || (() => openURL({url: URLS.configureCommands})); btn.onclick = btn.onclick || (() => openURL({url: URLS.configureCommands}));
@ -700,3 +703,39 @@ function highlightEditedStyle() {
requestAnimationFrame(() => scrollElementIntoView(entry)); requestAnimationFrame(() => scrollElementIntoView(entry));
} }
} }
function embedOptions() {
let options = $('#stylus-embedded-options');
if (!options) {
options = document.createElement('iframe');
options.id = 'stylus-embedded-options';
options.src = '/options.html';
document.documentElement.appendChild(options);
}
options.focus();
}
function unembedOptions() {
const options = $('#stylus-embedded-options');
if (options) {
options.contentWindow.document.body.classList.add('scaleout');
options.classList.add('fadeout');
animateElement(options, {
className: 'fadeout',
onComplete: () => options.remove(),
});
}
}
router.watch({hash: '#stylus-options'}, state => {
if (state) {
embedOptions();
} else {
unembedOptions();
}
});
window.addEventListener('closeOptions', () => {
router.updateHash('');
});

View File

@ -125,10 +125,6 @@
"default_popup": "popup.html" "default_popup": "popup.html"
}, },
"default_locale": "en", "default_locale": "en",
"options_ui": {
"page": "options.html",
"chrome_style": false
},
"applications": { "applications": {
"gecko": { "gecko": {
"id": "{7a7a4a92-a2a0-41d1-9fd7-1e92480d612d}", "id": "{7a7a4a92-a2a0-41d1-9fd7-1e92480d612d}",

View File

@ -21,8 +21,8 @@
<script src="js/polyfill.js"></script> <script src="js/polyfill.js"></script>
<script src="js/dom.js"></script> <script src="js/dom.js"></script>
<script src="js/messaging.js"></script>
<script src="js/promisify.js"></script> <script src="js/promisify.js"></script>
<script src="js/messaging.js"></script>
<script src="js/msg.js"></script> <script src="js/msg.js"></script>
<script src="js/localization.js"></script> <script src="js/localization.js"></script>
<script src="js/prefs.js"></script> <script src="js/prefs.js"></script>
@ -33,6 +33,13 @@
</head> </head>
<body id="stylus-options"> <body id="stylus-options">
<div id="options-header">
<div id="options-title">
<div id="options-close-icon"><svg viewBox="0 0 20 20" class="svg-icon"><path d="M11.69,10l4.55,4.55-1.69,1.69L10,11.69,5.45,16.23,3.77,14.55,8.31,10,3.77,5.45,5.45,3.77,10,8.31l4.55-4.55,1.69,1.69Z"></path></svg></div>
Stylus</div>
</div>
<div id="options"> <div id="options">
<div class="block"> <div class="block">
@ -204,7 +211,7 @@
<div class="block" id="actions"> <div class="block" id="actions">
<button data-cmd="reset" i18n-text="optionsResetButton" i18n-title="optionsReset"></button> <button data-cmd="reset" i18n-text="optionsResetButton" i18n-title="optionsReset"></button>
<button data-cmd="open-manage" i18n-text="optionsOpenManager"></button> <button data-cmd="open-manage" i18n-text="styleCancelEditLabel"></button>
<div data-cmd="check-updates"> <div data-cmd="check-updates">
<button i18n-text="optionsCheck" i18n-title="optionsCheckUpdate"> <button i18n-text="optionsCheck" i18n-title="optionsCheckUpdate">
<span id="update-progress"></span> <span id="update-progress"></span>

View File

@ -1,36 +1,80 @@
html.opera { html {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
height: 100%; height: 100vh;
} background-color: none;
html.opera body {
width: auto;
} }
body { body {
background: #fff; background: none;
margin: 0; margin: 0;
font-family: "Helvetica Neue", Helvetica, sans-serif; font-family: "Helvetica Neue", Helvetica, sans-serif;
font-size: 12px; font-size: 12px;
min-width: 480px; display: flex;
flex-direction: column;
width: auto;
max-width: 800px; max-width: 800px;
width: max-content; max-height: calc(100vh - 32px);
overflow-x: hidden; border: 1px solid #999;
box-shadow: 0px 5px 15px 3px hsla(0, 0%, 0%, .35);
animation: scalein .25s ease-in-out;
} }
@supports (-moz-appearance:none) { body.scaleout {
body { animation: scaleout .25s ease-in-out;
--addons-page-left-padding: 6px; }
/* compensate 'html.firefox .block' padding-left */
width: calc(100% - var(--addons-page-left-padding)); #options {
/* match the default FF theme */ background: #fff;
background-color: #f9f9fa; overflow-y: auto;
} }
html.firefox .block {
padding-left: var(--addons-page-left-padding); #options-close-icon .svg-icon {
} fill: #666;
transition: fill .5s;
}
#options-close-icon:hover .svg-icon {
fill: #000;
}
#options-close-icon {
display: inline-flex;
cursor: pointer;
position: absolute;
right: 6px;
top: 6px;
}
#options-close-icon .svg-icon {
height: 20px;
width: 20px;
}
#options-title {
font-weight: bold;
background-color: rgb(145, 208, 198);
padding: .75rem 26px .75rem calc(30% + 4px);
font-size: 22px;
letter-spacing: .5px;
position: relative;
min-height: 42px;
box-sizing: border-box;
border-bottom: 1px solid #999;
}
#options-title::before {
content: "";
width: 0;
height: 0;
padding: 0 32px 32px 0;
background: url(/images/icon/32.png);
position: absolute;
left: 26px;
top: 0;
bottom: 0;
margin: auto;
} }
.firefox .chromium-only, .firefox .chromium-only,
@ -152,23 +196,20 @@ input[type="color"] {
#actions { #actions {
justify-content: space-around; justify-content: space-around;
align-items: stretch; align-items: stretch;
padding: 1em; flex-wrap: wrap;
padding: .5em 1em 1em;
white-space: nowrap; white-space: nowrap;
background-color: rgba(0, 0, 0, .05); background-color: rgba(0, 0, 0, .05);
margin: 0; margin: 0;
} }
.firefox #actions,
.opera #actions {
background-color: transparent;
}
#actions button { #actions button {
width: auto; width: auto;
margin-top: .5em;
} }
#actions button:not(:last-child) { #actions button:not(:last-child) {
margin-right: 8px; margin-right: 4px;
} }
[data-cmd="check-updates"] button { [data-cmd="check-updates"] button {
@ -298,13 +339,16 @@ html:not(.firefox):not(.opera) #updates {
fill: #000; fill: #000;
} }
#message-box.note > div { #message-box.note {
max-width: calc(100vw - 6rem); align-items: center;
justify-content: center;
} }
.opera #message-box.note, #message-box.note > div {
.firefox #message-box.note { max-width: calc(100% - 5rem);
background-color: transparent; top: unset;
right: unset;
position: relative;
} }
@keyframes fadeinout { @keyframes fadeinout {
@ -336,6 +380,21 @@ html:not(.firefox):not(.opera) #updates {
text-transform: uppercase; text-transform: uppercase;
} }
.sync-options .actions { .sync-options .actions button {
padding-top: 6px; margin-top: .5em;
}
@keyframes scalein {
0% {
transform: scale3d(.3, .3, .3);
}
100% {
transform: scale3d(1, 1, 1);
}
}
@keyframes scaleout {
100% {
transform: scale3d(0, 0, 0);
}
} }

View File

@ -39,6 +39,10 @@ if (FIREFOX && 'update' in (chrome.commands || {})) {
} }
// actions // actions
$('#options-close-icon').onclick = () => {
top.dispatchEvent(new CustomEvent('closeOptions'));
};
document.onclick = e => { document.onclick = e => {
const target = e.target.closest('[data-cmd]'); const target = e.target.closest('[data-cmd]');
if (!target) { if (!target) {
@ -49,7 +53,7 @@ document.onclick = e => {
switch (target.dataset.cmd) { switch (target.dataset.cmd) {
case 'open-manage': case 'open-manage':
openURL({url: 'manage.html'}); API.openManage();
break; break;
case 'check-updates': case 'check-updates':
@ -292,3 +296,9 @@ function customizeHotkeys() {
} }
} }
} }
window.onkeydown = event => {
if (event.keyCode === 27) {
top.dispatchEvent(new CustomEvent('closeOptions'));
}
};

View File

@ -243,7 +243,7 @@
<div id="popup-options"> <div id="popup-options">
<button id="popup-manage-button" i18n-text="openManage" <button id="popup-manage-button" i18n-text="openManage"
data-href="manage.html" i18n-title="popupManageTooltip"></button> data-href="manage.html" i18n-title="popupManageTooltip"></button>
<button id="popup-options-button" i18n-text="openOptionsPopup"></button> <button id="popup-options-button" i18n-text="openOptions"></button>
<button id="popup-wiki-button" <button id="popup-wiki-button"
i18n-text="linkStylusWiki" i18n-text="linkStylusWiki"
i18n-title="linkGetHelp" i18n-title="linkGetHelp"

View File

@ -1,5 +1,5 @@
/* global configDialog hotkeys onTabReady msg /* global configDialog hotkeys onTabReady msg
getActiveTab FIREFOX getTabRealURL URLS API onDOMready $ $$ prefs CHROME getActiveTab FIREFOX getTabRealURL URLS API onDOMready $ $$ prefs
setupLivePrefs template t $create animateElement setupLivePrefs template t $create animateElement
tryJSONparse debounce CHROME_HAS_BORDER_BUG */ tryJSONparse debounce CHROME_HAS_BORDER_BUG */
@ -7,6 +7,7 @@
let installed; let installed;
let tabURL; let tabURL;
let unsupportedURL;
const handleEvent = {}; const handleEvent = {};
const ENTRY_ID_PREFIX_RAW = 'style-'; const ENTRY_ID_PREFIX_RAW = 'style-';
@ -28,6 +29,8 @@ getActiveTab()
.then(([results]) => { .then(([results]) => {
if (!results) { if (!results) {
// unsupported URL; // unsupported URL;
unsupportedURL = true;
$('#popup-manage-button').removeAttribute('title');
return; return;
} }
showStyles(results.map(r => Object.assign(r.data, r))); showStyles(results.map(r => Object.assign(r.data, r)));
@ -99,7 +102,7 @@ function initPopup() {
}); });
$('#popup-options-button').onclick = () => { $('#popup-options-button').onclick = () => {
chrome.runtime.openOptionsPage(); API.openManage({options: true});
window.close(); window.close();
}; };
@ -180,7 +183,7 @@ function initPopup() {
? new URL(tabURL).pathname.slice(1) ? new URL(tabURL).pathname.slice(1)
// this&nbsp;URL // this&nbsp;URL
: t('writeStyleForURL').replace(/ /g, '\u00a0'), : t('writeStyleForURL').replace(/ /g, '\u00a0'),
onclick: handleEvent.openLink, onclick: e => handleEvent.openEditor(e, {'url-prefix': tabURL}),
}); });
if (prefs.get('popup.breadcrumbs')) { if (prefs.get('popup.breadcrumbs')) {
urlLink.onmouseenter = urlLink.onmouseenter =
@ -203,7 +206,7 @@ function initPopup() {
href: 'edit.html?domain=' + encodeURIComponent(domain), href: 'edit.html?domain=' + encodeURIComponent(domain),
textContent: numParts > 2 ? domain.split('.')[0] : domain, textContent: numParts > 2 ? domain.split('.')[0] : domain,
title: `domain("${domain}")`, title: `domain("${domain}")`,
onclick: handleEvent.openLink, onclick: e => handleEvent.openEditor(e, {domain}),
}); });
domainLink.setAttribute('subdomain', numParts > 1 ? 'true' : ''); domainLink.setAttribute('subdomain', numParts > 1 ? 'true' : '');
matchTargets.appendChild(domainLink); matchTargets.appendChild(domainLink);
@ -289,7 +292,7 @@ function createStyleElement(style) {
const editLink = $('.style-edit-link', entry); const editLink = $('.style-edit-link', entry);
Object.assign(editLink, { Object.assign(editLink, {
href: editLink.getAttribute('href') + style.id, href: editLink.getAttribute('href') + style.id,
onclick: handleEvent.openLink, onclick: e => handleEvent.openEditor(e, {id: style.id}),
}); });
const styleName = $('.style-name', entry); const styleName = $('.style-name', entry);
Object.assign(styleName, { Object.assign(styleName, {
@ -528,18 +531,10 @@ Object.assign(handleEvent, {
$('#regexp-explanation').remove(); $('#regexp-explanation').remove();
}, },
openLink(event) { openEditor(event, options) {
if (!chrome.windows || !prefs.get('openEditInWindow', false)) {
handleEvent.openURLandHide.call(this, event);
return;
}
event.preventDefault(); event.preventDefault();
chrome.windows.create( API.openEditor(options);
Object.assign({ window.close();
url: this.href
}, prefs.get('windowPosition', {}))
);
close();
}, },
maybeEdit(event) { maybeEdit(event) {
@ -582,12 +577,16 @@ Object.assign(handleEvent, {
}, },
openManager(event) { openManager(event) {
if (event.button === 2 && unsupportedURL) return;
event.preventDefault(); event.preventDefault();
if (!this.eventHandled) { if (!this.eventHandled) {
// FIXME: this only works if popup is closed
this.eventHandled = true; this.eventHandled = true;
this.dataset.href += event.shiftKey || event.button === 2 ? API.openManage({
'?url=' + encodeURIComponent(tabURL) : ''; search: tabURL && (event.shiftKey || event.button === 2) ?
handleEvent.openURLandHide.call(this, event); `url:${tabURL}` : null
});
window.close();
} }
}, },

View File

@ -48,7 +48,8 @@ function uploadFileDropbox(client, stylesText) {
return client.filesUpload({path: '/' + DROPBOX_FILE, contents: stylesText}); return client.filesUpload({path: '/' + DROPBOX_FILE, contents: stylesText});
} }
$('#sync-dropbox-export').onclick = () => { $('#sync-dropbox-export').onclick = event => {
event.preventDefault();
const mode = localStorage.installType; const mode = localStorage.installType;
const title = t('syncDropboxStyles'); const title = t('syncDropboxStyles');
const text = mode === 'normal' ? t('connectingDropbox') : t('connectingDropboxNotAllowed'); const text = mode === 'normal' ? t('connectingDropbox') : t('connectingDropboxNotAllowed');
@ -122,7 +123,8 @@ $('#sync-dropbox-export').onclick = () => {
}); });
}; };
$('#sync-dropbox-import').onclick = () => { $('#sync-dropbox-import').onclick = event => {
event.preventDefault();
const mode = localStorage.installType; const mode = localStorage.installType;
const title = t('retrieveDropboxSync'); const title = t('retrieveDropboxSync');
const text = mode === 'normal' ? t('connectingDropbox') : t('connectingDropboxNotAllowed'); const text = mode === 'normal' ? t('connectingDropbox') : t('connectingDropboxNotAllowed');