shuffle and tidy up options (#1406)

* move updates/sync to the top, theme to the bottom
* remove font override
* replace 'Back to manage' with 'Close'
* add a note for the built-in shortcuts UI in FF
- update button
+ confirm reset
* one button to connect/disconnect
* shorten ids
* simplify/extract sync js
* reuse :invalid style
This commit is contained in:
tophf 2022-02-18 03:47:22 +03:00 committed by GitHub
parent c9b8593830
commit 8d3e01e05a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 245 additions and 371 deletions

View File

@ -1163,7 +1163,7 @@
"message": "Reset options"
},
"optionsStylusThemes": {
"message": "Find a Stylus UI theme"
"message": "Click Stylus icon in the browser toolbar on any Stylus page including this one, then click 'Find styles'"
},
"optionsSubheading": {
"message": "More Options",
@ -1500,6 +1500,9 @@
"shortcutsNote": {
"message": "Define keyboard shortcuts"
},
"shortcutsNoteFF": {
"message": "In Firefox 66+ you can open the built-in shortcuts UI manually:\n1) right-click Stylus icon in the toolbar and choose 'Manage'\n(alternatively, open about:addons via the main menu or Ctrl-Shift-A),\n2) in the page that opens click the cog wheel icon in the top right corner,\n3) choose 'Manage extension shortcuts'.\n\nYou can also customize the shortcuts here."
},
"sortDateNewestFirst": {
"message": "newest first",
"description": "Text added to indicate that sorting a date would add the newest entries at the top"

View File

@ -215,10 +215,6 @@ label {
#options span .svg-icon {
margin-top: -3px; /* inline info and config icons */
}
input:invalid {
background-color: rgba(255, 0, 0, 0.1);
color: darkred;
}
#enabled {
margin-left: 0;
}

View File

@ -116,6 +116,11 @@ input[type=search] {
border: 1px solid var(--c65);
}
input:invalid {
background-color: rgba(255, 0, 0, 0.1);
color: darkred;
}
.svg-icon {
cursor: pointer;
vertical-align: middle;

View File

@ -43,6 +43,7 @@ Object.assign(t, {
continue;
}
if (node.localName === 'template') {
node.remove();
t.createTemplate(node);
continue;
}
@ -87,17 +88,20 @@ Object.assign(t, {
text.replace(t.RX_WORD_BREAK, '$&\u00AD');
},
createTemplate(node) {
const el = node.content.firstElementChild.cloneNode(true);
t.NodeList(el);
t.template[node.dataset.id] = el;
// compress inter-tag whitespace to reduce number of DOM nodes by 25%
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
createTemplate(el) {
const {content} = el;
const toRemove = [];
// Compress inter-tag whitespace to reduce DOM tree and avoid space between elements without flex
const walker = document.createTreeWalker(content, NodeFilter.SHOW_TEXT);
for (let n; (n = walker.nextNode());) {
if (!/[\xA0\S]/.test(n.textContent)) { // allow \xA0 to keep  
n.remove();
if (!/[\xA0\S]/.test(n.textContent) || // allowing \xA0 so as to preserve  
n.nodeType === Node.COMMENT_NODE) {
toRemove.push(n);
}
}
toRemove.forEach(n => n.remove());
t.NodeList(content.querySelectorAll('*'));
t.template[el.dataset.id] = content.childNodes.length > 1 ? content : content.childNodes[0];
},
createText(str) {

View File

@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title i18n-text-append="optionsHeading">Stylus </title>
<link rel="stylesheet" href="global.css">
<link href="global-dark.css" rel="stylesheet">
<link href="global-dark.css?bugfix" rel="stylesheet"> <!-- https://crbug.com/1298600 -->
<script src="js/polyfill.js"></script>
<script src="js/toolbox.js"></script>
@ -19,6 +19,30 @@
<link rel="stylesheet" href="options/onoffswitch.css">
<link rel="stylesheet" href="options/options.css">
<template data-id="shortcutsFF">
<p style="line-height: 1.5" i18n-text="shortcutsNoteFF"></p>
<table style="margin: 0 auto">
<tr>
<td i18n-text="optionsCustomizePopup"></td>
<td><input id="hotkey._execute_browser_action" type="search"></td>
</tr>
<tr>
<td i18n-text="openManage"></td>
<td><input id="hotkey.openManage" type="search"></td>
</tr>
<tr>
<td i18n-text="disableAllStyles"></td>
<td><input id="hotkey.styleDisableAll" type="search"></td>
</tr>
</table>
<p style="text-align: center">
<a href="https://developer.mozilla.org/Add-ons/WebExtensions/manifest.json/commands#Key_combinations"
target="_blank" i18n-text="helpAlt"></a>
</p>
</template>
<script src="options/options-sync.js"></script>
<script src="js/dark-themer.js"></script> <!-- must be last in HEAD to avoid FOUC -->
</head>
@ -38,12 +62,55 @@
<div class="options-wrapper">
<div class="block">
<h1 i18n-text="cm_theme"></h1>
<div class="block" id="updates">
<h1 i18n-text="optionsCustomizeUpdate"></h1>
<div class="items">
<div class="label">
<a i18n-text="optionsStylusThemes" target="_blank"
href="https://33kk.github.io/uso-archive/?category=chrome-extension&search=Stylus"></a>
<label>
<span i18n-text="optionsUpdateInterval">
<a i18n-title="optionsUpdateImportNote"
data-cmd="note" class="svg-inline-wrapper" tabindex="0">
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
</a>
</span>
<input type="number" min="0" id="updateInterval">
</label>
</div>
</div>
<div class="block sync-options">
<h1 i18n-text="optionsCustomizeSync"></h1>
<div class="items">
<label>
<span class="sync-status"></span>
<div class="select-resizer">
<select class="cloud-name">
<option value="none" i18n-text="optionsSyncNone"></option>
<option value="dropbox">Dropbox</option>
<option value="google">Google Drive</option>
<option value="onedrive">OneDrive</option>
<option value="webdav">WebDAV</option>
</select>
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
</div>
</label>
<table class="drive-options" data-drive="webdav">
<tr>
<td i18n-text="optionsSyncUrl"></td>
<td><input type="url" data-option="url"></td>
</tr>
<tr>
<td i18n-text="optionsSyncUsername"></td>
<td><input type="text" data-option="username"></td>
</tr>
<tr>
<td i18n-text="optionsSyncPassword"></td>
<td><input type="password" data-option="password"></td>
</tr>
</table>
<div class="actions">
<button class="connect"></button>
<button class="sync-now" i18n-text="optionsSyncSyncNow"></button>
<button class="sync-login" i18n-text="optionsSyncLogin"></button>
</div>
</div>
</div>
@ -207,62 +274,6 @@
</div>
</div>
<div class="block" id="updates">
<h1 i18n-text="optionsCustomizeUpdate"></h1>
<div class="items">
<label>
<span i18n-text="optionsUpdateInterval">
<a i18n-title="optionsUpdateImportNote"
data-cmd="note" class="svg-inline-wrapper" tabindex="0">
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
</a>
</span>
<input type="number" min="0" id="updateInterval">
</label>
</div>
</div>
<div class="block sync-options">
<h1 i18n-text="optionsCustomizeSync"></h1>
<div class="items">
<div class="label">
<span class="sync-status"></span>
<div class="select-resizer">
<select class="cloud-name">
<option value="none" i18n-text="optionsSyncNone"></option>
<option value="dropbox">Dropbox</option>
<option value="google">Google Drive</option>
<option value="onedrive">OneDrive</option>
<option value="webdav">WebDAV</option>
</select>
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
</div>
</div>
<fieldset class="drive-options">
<div class="webdav-options" data-drive="webdav">
<label class="url">
<span i18n-text="optionsSyncUrl"></span>
<input type="text" data-option="url">
</label>
<label class="username">
<span i18n-text="optionsSyncUsername"></span>
<input type="text" data-option="username">
</label>
<label class="password">
<span i18n-text="optionsSyncPassword"></span>
<input type="password" data-option="password">
</label>
</div>
</fieldset>
<div class="actions">
<button type="button" class="connect" i18n-text="optionsSyncConnect"></button>
<button type="button" class="disconnect" i18n-text="optionsSyncDisconnect"></button>
<button type="button" class="sync-now" i18n-text="optionsSyncSyncNow"></button>
<button type="button" class="sync-login" i18n-text="optionsSyncLogin"></button>
</div>
</div>
</div>
<div class="block" id="advanced">
<h1 i18n-text="optionsAdvanced"></h1>
<div class="items">
@ -325,18 +336,16 @@
</div>
</div>
<div class="block">
<h1 i18n-text="cm_theme"></h1>
<div class="items" i18n-text="optionsStylusThemes" style="width:0"></div>
</div>
</div>
<div class="block" id="actions">
<button data-cmd="reset" i18n-text="optionsResetButton" i18n-title="optionsReset"></button>
<button data-cmd="open-manage" i18n-text="styleCancelEditLabel"></button>
<div data-cmd="check-updates">
<button i18n-text="optionsCheck" i18n-title="optionsCheckUpdate">
<span id="update-progress"></span>
</button>
<div id="updates-installed" i18n-text="updatesCurrentlyInstalled"></div>
</div>
<button data-cmd="open-keyboard" class="chromium-only" i18n-text="shortcuts" i18n-title="shortcutsNote"></button>
<button id="reset" i18n-text="optionsResetButton" i18n-title="optionsReset"></button>
<button id="shortcuts" i18n-text="shortcuts" i18n-title="shortcutsNote"></button>
<button id="manage" i18n-text="confirmClose"></button>
</div>
</div>

96
options/options-sync.js Normal file
View File

@ -0,0 +1,96 @@
/* global API msg */// msg.js
/* global t */// localization.js
/* global $ $$ toggleDataset waitForSelector */// dom.js
/* global capitalize */// toolbox.js
'use strict';
Promise.all([
API.sync.getStatus(),
waitForSelector('.sync-options'),
]).then(([status, elSync]) => {
const elCloud = $('.cloud-name', elSync);
const elToggle = $('.connect', elSync);
const elSyncNow = $('.sync-now', elSync);
const elStatus = $('.sync-status', elSync);
const elLogin = $('.sync-login', elSync);
const elDriveOptions = $$('.drive-options', elSync);
updateButtons();
msg.onExtension(e => {
if (e.method === 'syncStatusUpdate') {
setStatus(e.status);
}
});
elCloud.on('change', updateButtons);
elToggle.onclick = async () => {
if (elToggle.dataset.cmd === 'start') {
await API.sync.setDriveOptions(elCloud.value, getDriveOptions());
await API.sync.start(elCloud.value);
} else {
await API.sync.stop();
}
};
elSyncNow.onclick = API.sync.syncNow;
elLogin.onclick = async () => {
await API.sync.login();
await API.sync.syncNow();
};
function getDriveOptions() {
const result = {};
for (const el of $$(`[data-drive=${elCloud.value}] [data-option]`)) {
result[el.dataset.option] = el.value;
}
return result;
}
function setDriveOptions(options) {
for (const el of $$(`[data-drive=${elCloud.value}] [data-option]`)) {
el.value = options[el.dataset.option] || '';
}
}
function setStatus(newStatus) {
status = newStatus;
updateButtons();
}
async function updateButtons() {
const {state, STATES} = status;
const isConnected = state === STATES.connected;
const off = state === STATES.disconnected;
if (status.currentDriveName) {
elCloud.value = status.currentDriveName;
}
elCloud.disabled = !off;
elToggle.disabled = status.syncing;
elToggle.textContent = t(`optionsSync${off ? 'Connect' : 'Disconnect'}`);
elToggle.dataset.cmd = off ? 'start' : 'stop';
elSyncNow.disabled = !isConnected || status.syncing || !status.login;
elStatus.textContent = getStatusText();
elLogin.hidden = !isConnected || status.login;
for (const el of elDriveOptions) {
el.hidden = el.dataset.drive !== elCloud.value;
el.disabled = !off;
}
toggleDataset(elSync, 'enabled', elCloud.value !== 'none');
setDriveOptions(await API.sync.getDriveOptions(elCloud.value));
}
function getStatusText() {
if (status.syncing) {
const {phase, loaded, total} = status.progress || {};
return phase
? t(`optionsSyncStatus${capitalize(phase)}`, [loaded + 1, total], false) ||
`${phase} ${loaded} / ${total}`
: t('optionsSyncStatusSyncing');
}
const {state, errorMessage, STATES} = status;
if (errorMessage && (state === STATES.connected || state === STATES.disconnected)) {
return errorMessage;
}
if (state === STATES.connected && !status.login) {
return t('optionsSyncStatusRelogin');
}
return t(`optionsSyncStatus${capitalize(state)}`, null, false) || state;
}
});

View File

@ -8,8 +8,6 @@ html {
body {
background: none;
font-family: "Helvetica Neue", Helvetica, sans-serif;
font-size: 12px;
display: flex;
flex-direction: column;
width: auto;
@ -184,12 +182,6 @@ input[type=number] {
text-align: right;
}
input[type=number]:invalid,
input[type=text]:invalid {
background-color: rgba(255, 0, 0, 0.1);
color: darkred;
}
input[type="color"] {
box-sizing: border-box;
height: 2em;
@ -201,25 +193,16 @@ input[type=time] {
}
#actions {
justify-content: space-around;
align-items: stretch;
flex-wrap: wrap;
justify-content: center;
padding: .5em 1em 1em;
white-space: nowrap;
background-color: rgba(0, 0, 0, .05);
margin: 0;
border-top: 1px solid var(--c60);
border-bottom: none;
min-height: min-content; /* workaround for old Chrome ~70 bug when the window height is small */
}
#actions button {
width: auto;
margin-top: .5em;
}
#actions button:not(:last-child) {
margin-right: 4px;
margin: .5em 1em 0 0;
}
[data-cmd="check-updates"] button {
@ -229,41 +212,6 @@ input[type=time] {
padding: .5em 0 .5em 0;
cursor: pointer;
}
.update-in-progress [data-cmd="check-updates"] {
opacity: .5;
pointer-events: none;
}
.update-in-progress #update-progress {
position: absolute;
top: 0;
left: 0;
bottom: 0;
background-color: currentColor;
content: "";
opacity: .35;
}
#updates-installed {
position: absolute;
font-size: 85%;
margin-top: 1px;
}
#updates-installed::after {
content: attr(data-value);
margin-left: .5ex;
font-weight: bold;
}
#updates-installed:not([data-value]),
#updates-installed[data-value=""] {
display: none;
}
html:not(.firefox):not(.opera) #updates {
margin-bottom: 0;
}
.svg-inline-wrapper .svg-icon {
width: 16px;
@ -298,24 +246,27 @@ html:not(.firefox):not(.opera) #updates {
.sync-status::first-letter {
text-transform: uppercase;
}
.sync-options .drive-options {
margin: 0;
padding: 0;
border: 0;
[data-drive="webdav"] {
width: 100%;
border-spacing: 0;
border-collapse: collapse;
}
.drive-options > :not([hidden]) {
display: table;
[data-drive="webdav"] td:nth-child(1) {
padding: 1px .5em 1px 0;
max-width: 10em;
overflow-wrap: break-word;
}
[data-drive="webdav"] td:nth-child(2) {
padding: 1px 0;
width: 100%;
}
.drive-options > * > label {
display: table-row;
}
.drive-options > * > label > * {
display: table-cell;
}
.drive-options > * input {
[data-drive="webdav"] input {
width: 100%;
box-sizing: border-box;
line-height: 1.5;
}
.sync-options:not([data-enabled]) .actions {
display: none;
}
.sync-options .actions button {
margin-top: .5em;

View File

@ -1,20 +1,11 @@
/* global API msg */// msg.js
/* global API */// msg.js
/* global prefs */
/* global t */// localization.js
/* global
$
$$
$create
$createLink
getEventKeyName
messageBoxProxy
setupLivePrefs
*/// dom.js
/* global $ $$ getEventKeyName messageBoxProxy setupLivePrefs */// dom.js
/* global
CHROME_POPUP_BORDER_BUG
FIREFOX
URLS
capitalize
clamp
ignoreChromeError
openURL
@ -23,242 +14,61 @@
setupLivePrefs();
$$('input[min], input[max]').forEach(enforceInputRange);
if (CHROME_POPUP_BORDER_BUG) {
$('.chrome-no-popup-border').classList.remove('chrome-no-popup-border');
}
if (FIREFOX && 'update' in (chrome.commands || {})) {
$('[data-cmd="open-keyboard"]').classList.remove('chromium-only');
$('#shortcuts').classList.remove('chromium-only');
}
// actions
$('#options-close-icon').onclick = () => {
top.dispatchEvent(new CustomEvent('closeOptions'));
};
document.onclick = e => {
const target = e.target.closest('[data-cmd]');
if (!target) {
return;
$('#manage').onclick = () => {
API.openManage();
};
$('#shortcuts').onclick = () => {
if (FIREFOX) {
customizeHotkeys();
} else {
openURL({url: URLS.configureCommands});
}
// prevent double-triggering in case a sub-element was clicked
e.stopPropagation();
switch (target.dataset.cmd) {
case 'open-manage':
API.openManage();
break;
case 'check-updates':
checkUpdates();
break;
case 'open-keyboard':
if (FIREFOX) {
customizeHotkeys();
} else {
openURL({url: URLS.configureCommands});
};
$('#reset').onclick = async () => {
if (await messageBoxProxy.confirm(t('confirmDiscardChanges'))) {
for (const el of $$('input')) {
const id = el.id || el.name;
if (prefs.knownKeys.includes(id)) {
prefs.reset(id);
}
e.preventDefault();
break;
case 'reset':
$$('input')
.filter(input => prefs.knownKeys.includes(input.id))
.forEach(input => prefs.reset(input.id));
break;
}
}
};
// sync to cloud
(() => {
const elCloud = $('.sync-options .cloud-name');
const elStart = $('.sync-options .connect');
const elStop = $('.sync-options .disconnect');
const elSyncNow = $('.sync-options .sync-now');
const elStatus = $('.sync-options .sync-status');
const elLogin = $('.sync-options .sync-login');
const elDriveOptions = $('.sync-options .drive-options');
/** @type {Sync.Status} */
let status = {};
msg.onExtension(e => {
if (e.method === 'syncStatusUpdate') {
setStatus(e.status);
}
});
API.sync.getStatus()
.then(setStatus);
elCloud.on('change', updateButtons);
for (const [btn, fn] of [
[elStart, async () => {
await API.sync.setDriveOptions(elCloud.value, getDriveOptions());
await API.sync.start(elCloud.value);
}],
[elStop, API.sync.stop],
[elSyncNow, API.sync.syncNow],
[elLogin, async () => {
await API.sync.login();
await API.sync.syncNow();
}],
]) {
btn.on('click', e => {
if (getEventKeyName(e) === 'MouseL') {
fn();
}
});
}
function getDriveOptions() {
const result = {};
for (const el of $$(`[data-drive=${elCloud.value}] [data-option]`)) {
result[el.dataset.option] = el.value;
}
return result;
}
function setDriveOptions(options) {
for (const el of $$(`[data-drive=${elCloud.value}] [data-option]`)) {
el.value = options[el.dataset.option] || '';
}
}
function setStatus(newStatus) {
status = newStatus;
updateButtons();
}
async function updateButtons() {
const {state, STATES} = status;
const isConnected = state === STATES.connected;
const isDisconnected = state === STATES.disconnected;
if (status.currentDriveName) {
elCloud.value = status.currentDriveName;
}
for (const [el, enable] of [
[elCloud, isDisconnected],
[elDriveOptions, isDisconnected],
[elStart, isDisconnected && elCloud.value !== 'none'],
[elStop, isConnected && !status.syncing],
[elSyncNow, isConnected && !status.syncing && status.login],
]) {
el.disabled = !enable;
}
elStatus.textContent = getStatusText();
elLogin.hidden = !isConnected || status.login;
for (const el of elDriveOptions.children) {
el.hidden = el.dataset.drive !== elCloud.value;
}
setDriveOptions(await API.sync.getDriveOptions(elCloud.value));
}
function getStatusText() {
if (status.syncing) {
const {phase, loaded, total} = status.progress || {};
return phase
? t(`optionsSyncStatus${capitalize(phase)}`, [loaded + 1, total], false) ||
`${phase} ${loaded} / ${total}`
: t('optionsSyncStatusSyncing');
}
const {state, errorMessage, STATES} = status;
if (errorMessage && (state === STATES.connected || state === STATES.disconnected)) {
return errorMessage;
}
if (state === STATES.connected && !status.login) {
return t('optionsSyncStatusRelogin');
}
return t(`optionsSyncStatus${capitalize(state)}`, null, false) || state;
}
})();
function checkUpdates() {
let total = 0;
let checked = 0;
let updated = 0;
const maxWidth = $('#update-progress').parentElement.clientWidth;
chrome.runtime.onConnect.addListener(function onConnect(port) {
if (port.name !== 'updater') return;
port.onMessage.addListener(observer);
chrome.runtime.onConnect.removeListener(onConnect);
});
API.updater.checkAllStyles({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 || '';
}
}
function customizeHotkeys() {
// command name -> i18n id
const hotkeys = new Map([
['_execute_browser_action', 'optionsCustomizePopup'],
['openManage', 'openManage'],
['styleDisableAll', 'disableAllStyles'],
]);
messageBoxProxy.show({
title: t('shortcutsNote'),
contents: [
$create('table',
[...hotkeys.entries()].map(([cmd, i18n]) =>
$create('tr', [
$create('td', t(i18n)),
$create('td',
$create('input', {
id: 'hotkey.' + cmd,
type: 'search',
//placeholder: t('helpKeyMapHotkey'),
})),
]))),
],
className: 'center',
contents: t.template.shortcutsFF.cloneNode(true),
className: 'center-dialog pre-line',
buttons: [t('confirmClose')],
onshow(box) {
const ids = [];
for (const cmd of hotkeys.keys()) {
const id = 'hotkey.' + cmd;
ids.push(id);
$('#' + id).oninput = onInput;
}
setupLivePrefs(ids);
$('button', box).insertAdjacentElement('beforebegin',
$createLink(
'https://developer.mozilla.org/Add-ons/WebExtensions/manifest.json/commands#Key_combinations',
t('helpAlt')));
box.oninput = onInput;
setupLivePrefs($$('input', box).map(el => el.id));
},
});
function onInput() {
const name = this.id.split('.')[1];
const shortcut = this.value.trim();
async function onInput({target: el}) {
const name = el.id.split('.')[1];
const shortcut = el.value.trim();
if (!shortcut) {
browser.commands.reset(name).catch(ignoreChromeError);
this.setCustomValidity('');
el.setCustomValidity('');
return;
}
try {
browser.commands.update({name, shortcut}).then(
() => this.setCustomValidity(''),
err => this.setCustomValidity(err)
);
await browser.commands.update({name, shortcut});
el.setCustomValidity('');
} catch (err) {
this.setCustomValidity(err);
el.setCustomValidity(err);
}
}
}