tidy up USW-related UI and code (#1285)
* shortened the title to "Publish" and fixed the compact mode * made collapsed <details> share the same line in compact mode * made hard-coded strings localizable * IIFE modules instead of generically named globals * unified and sorted the names of localized messages * adjusted spacing of header items * center auth popup to current window Co-authored-by: Gusted <williamzijl7@hotmail.com>
This commit is contained in:
parent
23d86c53a7
commit
6650a37194
|
@ -452,6 +452,9 @@
|
|||
"message": "Clone",
|
||||
"description": "Used in various places for an action that clones something"
|
||||
},
|
||||
"genericDescription": {
|
||||
"message": "Description"
|
||||
},
|
||||
"genericDisabledLabel": {
|
||||
"message": "Disabled",
|
||||
"description": "Used in various lists/options to indicate that something is disabled"
|
||||
|
@ -1242,6 +1245,28 @@
|
|||
"message": "Temporarily applies the changes without saving.\nSave the style to make the changes permanent.",
|
||||
"description": "Tooltip for the checkbox in style editor to enable live preview while editing."
|
||||
},
|
||||
"publish": {
|
||||
"message": "Publish",
|
||||
"description": "Header for the section to link the style with userStyles.world"
|
||||
},
|
||||
"publishPush": {
|
||||
"message": "Push update",
|
||||
"description": "The 'Publish style' button's new name when a connection is established"
|
||||
},
|
||||
"publishReconnect": {
|
||||
"message": "Try disconnecting then publish again"
|
||||
},
|
||||
"publishRetry": {
|
||||
"message": "Stylus is still trying to publish this style, but you can retry if you see no authentication activity or popups. Retry now?"
|
||||
},
|
||||
"publishStyle": {
|
||||
"message": "Publish style",
|
||||
"description": "Publish the current style to userstyles.world"
|
||||
},
|
||||
"publishUsw": {
|
||||
"message": "Using <userstyles.world>",
|
||||
"description": "Name of the link to https://userstyles.world in the editor"
|
||||
},
|
||||
"readingStyles": {
|
||||
"message": "Reading styles..."
|
||||
},
|
||||
|
@ -1365,18 +1390,6 @@
|
|||
"message": "Sections",
|
||||
"description": "Header for the table of contents block listing style section names in the left panel of the classic editor"
|
||||
},
|
||||
"integration": {
|
||||
"message": "UserStyles.world integration",
|
||||
"description": "Header for the section to link the style with userStyles.world"
|
||||
},
|
||||
"uploadStyle": {
|
||||
"message": "Publish style",
|
||||
"description": "Publish the current style to userstyles.world"
|
||||
},
|
||||
"revokeLink": {
|
||||
"message": "Revoke link",
|
||||
"description": "Revoke current link of style with userstyles.world"
|
||||
},
|
||||
"shortcuts": {
|
||||
"message": "Shortcuts",
|
||||
"description": "Go to shortcut configuration"
|
||||
|
@ -1493,6 +1506,9 @@
|
|||
"message": "Mozilla Format",
|
||||
"description": "Heading for the section with buttons to import/export Mozilla format of the style"
|
||||
},
|
||||
"styleName": {
|
||||
"message": "Style name"
|
||||
},
|
||||
"styleNotAppliedRegexpProblemTooltip": {
|
||||
"message": "Style was not applied due to its incorrect usage of 'regexp()'",
|
||||
"description": "Tooltip in the popup for styles that were not applied at all"
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
/* global syncMan */
|
||||
/* global updateMan */
|
||||
/* global usercssMan */
|
||||
/* global uswApi */
|
||||
/* global
|
||||
FIREFOX
|
||||
URLS
|
||||
|
@ -20,10 +21,26 @@
|
|||
|
||||
addAPI(/** @namespace API */ {
|
||||
|
||||
/** Temporary storage for data needed elsewhere e.g. in a content script */
|
||||
data: ((data = {}) => ({
|
||||
del: key => delete data[key],
|
||||
get: key => data[key],
|
||||
has: key => key in data,
|
||||
pop: key => {
|
||||
const val = data[key];
|
||||
delete data[key];
|
||||
return val;
|
||||
},
|
||||
set: (key, val) => {
|
||||
data[key] = val;
|
||||
},
|
||||
}))(),
|
||||
|
||||
styles: styleMan,
|
||||
sync: syncMan,
|
||||
updater: updateMan,
|
||||
usercss: usercssMan,
|
||||
usw: uswApi,
|
||||
/** @type {BackgroundWorker} */
|
||||
worker: createWorker({url: '/background/background-worker'}),
|
||||
|
||||
|
|
|
@ -6,8 +6,6 @@
|
|||
/* global prefs */
|
||||
/* global tabMan */
|
||||
/* global usercssMan */
|
||||
/* global tokenMan */
|
||||
/* global retrieveStyleInformation uploadStyle */// usw-api.js
|
||||
'use strict';
|
||||
|
||||
/*
|
||||
|
@ -63,7 +61,6 @@ const styleMan = (() => {
|
|||
let ready = init();
|
||||
|
||||
chrome.runtime.onConnect.addListener(handleLivePreview);
|
||||
chrome.runtime.onConnect.addListener(handlePublishingUSW);
|
||||
|
||||
//#endregion
|
||||
//#region Exports
|
||||
|
@ -74,16 +71,21 @@ const styleMan = (() => {
|
|||
async delete(id, reason) {
|
||||
if (ready.then) await ready;
|
||||
const data = id2data(id);
|
||||
const {style, appliesTo} = data;
|
||||
await db.exec('delete', id);
|
||||
if (reason !== 'sync') {
|
||||
API.sync.delete(data.style._id, Date.now());
|
||||
API.sync.delete(style._id, Date.now());
|
||||
}
|
||||
for (const url of data.appliesTo) {
|
||||
for (const url of appliesTo) {
|
||||
const cache = cachedStyleForUrl.get(url);
|
||||
if (cache) delete cache.sections[id];
|
||||
}
|
||||
dataMap.delete(id);
|
||||
uuidIndex.delete(data.style._id);
|
||||
uuidIndex.delete(style._id);
|
||||
if (style._usw && style._usw.token) {
|
||||
// Must be called after the style is deleted from dataMap
|
||||
API.usw.revoke(id);
|
||||
}
|
||||
await msg.broadcast({
|
||||
method: 'styleDeleted',
|
||||
style: {id},
|
||||
|
@ -107,7 +109,7 @@ const styleMan = (() => {
|
|||
if (ready.then) await ready;
|
||||
style = mergeWithMapped(style);
|
||||
style.updateDate = Date.now();
|
||||
return handleSave(await saveStyle(style), {reason: 'editSave'});
|
||||
return saveStyle(style, {reason: 'editSave'});
|
||||
},
|
||||
|
||||
/** @returns {Promise<?StyleObj>} */
|
||||
|
@ -240,7 +242,7 @@ const styleMan = (() => {
|
|||
if (url) style.url = style.installationUrl = url;
|
||||
style.originalDigest = await calcStyleDigest(style);
|
||||
// FIXME: update updateDate? what about usercss config?
|
||||
return handleSave(await saveStyle(style), {reason});
|
||||
return saveStyle(style, {reason});
|
||||
},
|
||||
|
||||
/** @returns {Promise<?StyleObj>} */
|
||||
|
@ -268,11 +270,13 @@ const styleMan = (() => {
|
|||
}
|
||||
},
|
||||
|
||||
save: saveStyle,
|
||||
|
||||
/** @returns {Promise<number>} style id */
|
||||
async toggle(id, enabled) {
|
||||
if (ready.then) await ready;
|
||||
const style = Object.assign({}, id2style(id), {enabled});
|
||||
handleSave(await saveStyle(style), {reason: 'toggle', codeIsUpdated: false});
|
||||
await saveStyle(style, {reason: 'toggle', codeIsUpdated: false});
|
||||
return id;
|
||||
},
|
||||
|
||||
|
@ -356,65 +360,6 @@ const styleMan = (() => {
|
|||
});
|
||||
}
|
||||
|
||||
function handlePublishingUSW(port) {
|
||||
if (port.name !== 'link-style-usw') {
|
||||
return;
|
||||
}
|
||||
port.onMessage.addListener(async incData => {
|
||||
const {data: style, reason} = incData;
|
||||
if (!style.id) {
|
||||
return;
|
||||
}
|
||||
switch (reason) {
|
||||
case 'revoke':
|
||||
await tokenMan.revokeToken('userstylesworld', style.id);
|
||||
style._usw = {};
|
||||
handleSave(await saveStyle(style), {reason: 'success-revoke', codeIsUpdated: true});
|
||||
break;
|
||||
|
||||
case 'publish': {
|
||||
if (!style._usw || !style._usw.token) {
|
||||
for (const {style: someStyle} of dataMap.values()) {
|
||||
if (someStyle._id === style._id) {
|
||||
someStyle.tmpSourceCode = style.sourceCode;
|
||||
let metadata = {};
|
||||
try {
|
||||
const {metadata: tmpMetadata} = await API.worker.parseUsercssMeta(style.sourceCode);
|
||||
metadata = tmpMetadata;
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
someStyle.metadata = metadata;
|
||||
} else {
|
||||
delete someStyle.tmpSourceCode;
|
||||
delete someStyle.metadata;
|
||||
}
|
||||
handleSave(await saveStyle(someStyle), {broadcast: false});
|
||||
}
|
||||
style._usw = {
|
||||
token: await tokenMan.getToken('userstylesworld', true, style.id),
|
||||
};
|
||||
|
||||
delete style.tmpSourceCode;
|
||||
delete style.metadata;
|
||||
for (const [k, v] of Object.entries(await retrieveStyleInformation(style._usw.token))) {
|
||||
style._usw[k] = v;
|
||||
}
|
||||
handleSave(await saveStyle(style), {reason: 'success-publishing', codeIsUpdated: true});
|
||||
}
|
||||
|
||||
const returnResult = await uploadStyle(style);
|
||||
// USw prefix errors with `Error:`.
|
||||
if (returnResult.startsWith('Error:')) {
|
||||
style._usw.publishingError = returnResult;
|
||||
handleSave(await saveStyle(style), {reason: 'publishing-failed', codeIsUpdated: true});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function addIncludeExclude(type, id, rule) {
|
||||
if (ready.then) await ready;
|
||||
const style = Object.assign({}, id2style(id));
|
||||
|
@ -423,7 +368,7 @@ const styleMan = (() => {
|
|||
throw new Error('The rule already exists');
|
||||
}
|
||||
style[type] = list.concat([rule]);
|
||||
return handleSave(await saveStyle(style), {reason: 'styleSettings'});
|
||||
return saveStyle(style, {reason: 'styleSettings'});
|
||||
}
|
||||
|
||||
async function removeIncludeExclude(type, id, rule) {
|
||||
|
@ -434,7 +379,7 @@ const styleMan = (() => {
|
|||
return;
|
||||
}
|
||||
style[type] = list.filter(r => r !== rule);
|
||||
return handleSave(await saveStyle(style), {reason: 'styleSettings'});
|
||||
return saveStyle(style, {reason: 'styleSettings'});
|
||||
}
|
||||
|
||||
function broadcastStyleUpdated(style, reason, method = 'styleUpdated', codeIsUpdated = true) {
|
||||
|
@ -490,14 +435,14 @@ const styleMan = (() => {
|
|||
style.id = newId;
|
||||
}
|
||||
uuidIndex.set(style._id, style.id);
|
||||
API.sync.put(style._id, style._rev, style._usw);
|
||||
API.sync.put(style._id, style._rev);
|
||||
}
|
||||
|
||||
async function saveStyle(style) {
|
||||
async function saveStyle(style, handlingOptions) {
|
||||
beforeSave(style);
|
||||
const newId = await db.exec('put', style);
|
||||
afterSave(style, newId);
|
||||
return style;
|
||||
return handleSave(style, handlingOptions);
|
||||
}
|
||||
|
||||
function handleSave(style, {reason, codeIsUpdated, broadcast = true}) {
|
||||
|
@ -528,9 +473,7 @@ const styleMan = (() => {
|
|||
|
||||
async function init() {
|
||||
const styles = await db.exec('getAll') || [];
|
||||
const updated = styles.filter(style =>
|
||||
addMissingProps(style) +
|
||||
addCustomName(style));
|
||||
const updated = styles.filter(fixOldStyleProps);
|
||||
if (updated.length) {
|
||||
await db.exec('putMany', updated);
|
||||
}
|
||||
|
@ -543,7 +486,7 @@ const styleMan = (() => {
|
|||
bgReady._resolveStyles();
|
||||
}
|
||||
|
||||
function addMissingProps(style) {
|
||||
function fixOldStyleProps(style) {
|
||||
let res = 0;
|
||||
for (const key in MISSING_PROPS) {
|
||||
if (!style[key]) {
|
||||
|
@ -551,20 +494,15 @@ const styleMan = (() => {
|
|||
res = 1;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
/** Upgrades the old way of customizing local names */
|
||||
function addCustomName(style) {
|
||||
let res = 0;
|
||||
/* Upgrade the old way of customizing local names */
|
||||
const {originalName} = style;
|
||||
if (originalName) {
|
||||
res = 1;
|
||||
if (originalName !== style.name) {
|
||||
style.customName = style.name;
|
||||
style.name = originalName;
|
||||
}
|
||||
delete style.originalName;
|
||||
res = 1;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
|
|
@ -64,11 +64,12 @@ const tokenMan = (() => {
|
|||
|
||||
return {
|
||||
|
||||
buildKeys(name, styleId) {
|
||||
buildKeys(name, hooks) {
|
||||
const prefix = `secure/token/${hooks ? hooks.keyName(name) : name}/`;
|
||||
const k = {
|
||||
TOKEN: `secure/token/${name}/${styleId ? `${styleId}/` : ''}token`,
|
||||
EXPIRE: `secure/token/${name}/${styleId ? `${styleId}/` : ''}expire`,
|
||||
REFRESH: `secure/token/${name}/${styleId ? `${styleId}/` : ''}refresh`,
|
||||
TOKEN: `${prefix}token`,
|
||||
EXPIRE: `${prefix}expire`,
|
||||
REFRESH: `${prefix}refresh`,
|
||||
};
|
||||
k.LIST = Object.values(k);
|
||||
return k;
|
||||
|
@ -78,8 +79,8 @@ const tokenMan = (() => {
|
|||
return AUTH[name].clientId;
|
||||
},
|
||||
|
||||
async getToken(name, interactive, styleId) {
|
||||
const k = tokenMan.buildKeys(name, styleId);
|
||||
async getToken(name, interactive, hooks) {
|
||||
const k = tokenMan.buildKeys(name, hooks);
|
||||
const obj = await chromeLocal.get(k.LIST);
|
||||
if (obj[k.TOKEN]) {
|
||||
if (!obj[k.EXPIRE] || Date.now() < obj[k.EXPIRE]) {
|
||||
|
@ -92,13 +93,12 @@ const tokenMan = (() => {
|
|||
if (!interactive) {
|
||||
throw new Error(`Invalid token: ${name}`);
|
||||
}
|
||||
const accessToken = authUser(name, k, interactive, styleId ? {vendor_data: styleId} : {});
|
||||
return accessToken;
|
||||
return authUser(k, name, interactive, hooks);
|
||||
},
|
||||
|
||||
async revokeToken(name, styleId) {
|
||||
async revokeToken(name, hooks) {
|
||||
const provider = AUTH[name];
|
||||
const k = tokenMan.buildKeys(name, styleId);
|
||||
const k = tokenMan.buildKeys(name, hooks);
|
||||
if (provider.revoke) {
|
||||
try {
|
||||
const token = await chromeLocal.getValue(k.TOKEN);
|
||||
|
@ -133,17 +133,17 @@ const tokenMan = (() => {
|
|||
return handleTokenResult(result, k);
|
||||
}
|
||||
|
||||
async function authUser(name, k, interactive = false, extraQuery = {}) {
|
||||
async function authUser(keys, name, interactive = false, hooks = null) {
|
||||
await require(['/vendor/webext-launch-web-auth-flow/webext-launch-web-auth-flow.min']);
|
||||
/* global webextLaunchWebAuthFlow */
|
||||
const provider = AUTH[name];
|
||||
const state = Math.random().toFixed(8).slice(2);
|
||||
const query = Object.assign(extraQuery, {
|
||||
const query = {
|
||||
response_type: provider.flow,
|
||||
client_id: provider.clientId,
|
||||
redirect_uri: provider.redirect_uri || chrome.identity.getRedirectURL(),
|
||||
state,
|
||||
});
|
||||
};
|
||||
if (provider.scopes) {
|
||||
query.scope = provider.scopes.join(' ');
|
||||
}
|
||||
|
@ -153,17 +153,25 @@ const tokenMan = (() => {
|
|||
if (alwaysUseTab == null) {
|
||||
alwaysUseTab = await detectVivaldiWebRequestBug();
|
||||
}
|
||||
if (hooks) hooks.query(query);
|
||||
const url = `${provider.authURL}?${new URLSearchParams(query)}`;
|
||||
const width = Math.min(screen.availWidth - 100, 800);
|
||||
const height = Math.min(screen.availHeight - 100, 800);
|
||||
const wnd = await browser.windows.getLastFocused();
|
||||
const finalUrl = await webextLaunchWebAuthFlow({
|
||||
url,
|
||||
alwaysUseTab,
|
||||
interactive,
|
||||
redirect_uri: query.redirect_uri,
|
||||
windowOptions: {
|
||||
windowOptions: Object.assign({
|
||||
state: 'normal',
|
||||
width: Math.min(screen.width - 100, 800),
|
||||
height: Math.min(screen.height - 100, 800),
|
||||
},
|
||||
width,
|
||||
height,
|
||||
}, wnd.state !== 'minimized' && {
|
||||
// Center the popup to the current window
|
||||
top: Math.ceil(wnd.top + (wnd.height - width) / 2),
|
||||
left: Math.ceil(wnd.left + (wnd.width - width) / 2),
|
||||
}),
|
||||
});
|
||||
const params = new URLSearchParams(
|
||||
provider.flow === 'token' ?
|
||||
|
@ -194,7 +202,7 @@ const tokenMan = (() => {
|
|||
}
|
||||
result = await postQuery(provider.tokenURL, body);
|
||||
}
|
||||
return handleTokenResult(result, k);
|
||||
return handleTokenResult(result, keys);
|
||||
}
|
||||
|
||||
async function handleTokenResult(result, k) {
|
||||
|
|
|
@ -1,29 +1,119 @@
|
|||
/* global API msg */// msg.js
|
||||
/* global URLS */ // toolbox.js
|
||||
|
||||
/* global tokenMan */
|
||||
'use strict';
|
||||
|
||||
/* exported retrieveStyleInformation */
|
||||
async function retrieveStyleInformation(token) {
|
||||
return (await (await fetch(`${URLS.usw}api/style`, {
|
||||
method: 'GET',
|
||||
headers: new Headers({
|
||||
'Authorization': `Bearer ${token}`,
|
||||
}),
|
||||
credentials: 'omit',
|
||||
})).json()).data;
|
||||
const uswApi = (() => {
|
||||
|
||||
//#region Internals
|
||||
|
||||
class TokenHooks {
|
||||
constructor(id) {
|
||||
this.id = id;
|
||||
}
|
||||
keyName(name) {
|
||||
return `${name}/${this.id}`;
|
||||
}
|
||||
query(query) {
|
||||
return Object.assign(query, {vendor_data: this.id});
|
||||
}
|
||||
}
|
||||
|
||||
/* exported uploadStyle */
|
||||
async function uploadStyle(style) {
|
||||
return (await (await fetch(`${URLS.usw}api/style/${style._usw.id}`, {
|
||||
method: 'POST',
|
||||
headers: new Headers({
|
||||
'Authorization': `Bearer ${style._usw.token}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: JSON.stringify({
|
||||
code: style.sourceCode,
|
||||
}),
|
||||
credentials: 'omit',
|
||||
})).json()).data;
|
||||
function fakeUsercssHeader(style) {
|
||||
const {name, _usw: u = {}} = style;
|
||||
const meta = Object.entries({
|
||||
'@name': u.name || name || '?',
|
||||
'@version': // Same as USO-archive version: YYYYMMDD.hh.mm
|
||||
new Date().toISOString().replace(/^(\d+)-(\d+)-(\d+)T(\d+):(\d+).+/, '$1$2$3.$4.$5'),
|
||||
'@namespace': u.namespace !== '?' && u.namespace ||
|
||||
u.username && `userstyles.world/user/${u.username}` ||
|
||||
'?',
|
||||
'@description': u.description,
|
||||
'@author': u.username,
|
||||
'@license': u.license,
|
||||
});
|
||||
const maxKeyLen = meta.reduce((res, [k]) => Math.max(res, k.length), 0);
|
||||
return [
|
||||
'/* ==UserStyle==',
|
||||
...meta.map(([k, v]) => `${k}${' '.repeat(maxKeyLen - k.length + 2)}${v || ''}`),
|
||||
'==/UserStyle== */',
|
||||
].join('\n') + '\n\n';
|
||||
}
|
||||
|
||||
async function linkStyle(style, sourceCode) {
|
||||
const {id} = style;
|
||||
const metadata = await API.worker.parseUsercssMeta(sourceCode).catch(console.warn) || {};
|
||||
const uswData = Object.assign({}, style, {metadata, sourceCode});
|
||||
API.data.set('usw' + id, uswData);
|
||||
const token = await tokenMan.getToken('userstylesworld', true, new TokenHooks(id));
|
||||
const info = await uswFetch('style', token);
|
||||
const data = style._usw = Object.assign({token}, info);
|
||||
style.url = style.url || data.homepage || `${URLS.usw}style/${data.id}`;
|
||||
await uswSave(style);
|
||||
return data;
|
||||
}
|
||||
|
||||
async function uswFetch(path, token, opts) {
|
||||
opts = Object.assign({credentials: 'omit'}, opts);
|
||||
opts.headers = Object.assign({Authorization: `Bearer ${token}`}, opts.headers);
|
||||
return (await (await fetch(`${URLS.usw}api/${path}`, opts)).json()).data;
|
||||
}
|
||||
|
||||
/** Uses a custom method when broadcasting and avoids needlessly sending the entire style */
|
||||
async function uswSave(style) {
|
||||
const {id, _usw} = style;
|
||||
await API.styles.save(style, {broadcast: false});
|
||||
msg.broadcastExtension({method: 'uswData', style: {id, _usw}});
|
||||
}
|
||||
|
||||
//#endregion
|
||||
//#region Exports
|
||||
|
||||
return {
|
||||
/**
|
||||
* @param {number} id
|
||||
* @param {string} sourceCode
|
||||
* @return {Promise<string>}
|
||||
*/
|
||||
async publish(id, sourceCode) {
|
||||
const style = await API.styles.get(id);
|
||||
const data = (style._usw || {}).token
|
||||
? style._usw
|
||||
: await linkStyle(style, sourceCode);
|
||||
const header = style.usercssData ? '' : fakeUsercssHeader(style);
|
||||
return uswFetch(`style/${data.id}`, data.token, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({code: header + sourceCode}),
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {number} id
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async revoke(id) {
|
||||
await tokenMan.revokeToken('userstylesworld', new TokenHooks(id));
|
||||
const style = await API.styles.get(id);
|
||||
if (style) {
|
||||
style._usw = {};
|
||||
await uswSave(style);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
//#endregion
|
||||
})();
|
||||
|
||||
/* Doing this outside so we don't break IDE's recognition of the exported methods in IIFE */
|
||||
for (const [k, fn] of Object.entries(uswApi)) {
|
||||
uswApi[k] = async (id, ...args) => {
|
||||
API.data.set('usw' + id, true);
|
||||
try {
|
||||
/* Awaiting inside `try` so that `finally` runs when done */
|
||||
return await fn(id, ...args);
|
||||
} finally {
|
||||
API.data.del('usw' + id);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -19,9 +19,8 @@
|
|||
|
||||
if (location.pathname === '/api/oauth/style/new') {
|
||||
const styleId = Number(new URLSearchParams(location.search).get('vendor_data'));
|
||||
API.styles.get(styleId).then(style => {
|
||||
style.sourceCode = style.tmpSourceCode;
|
||||
sendPostMessage({type: 'usw-fill-new-style', data: style});
|
||||
API.data.pop('usw' + styleId).then(data => {
|
||||
sendPostMessage({type: 'usw-fill-new-style', data});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
22
edit.html
22
edit.html
|
@ -242,7 +242,7 @@
|
|||
|
||||
<body id="stylus-edit">
|
||||
<div id="header">
|
||||
<h1 id="heading"> </h1> <!-- nbsp allocates the actual height which prevents page shift -->
|
||||
<h1 id="heading" i18n-data-edit="editStyleHeading" i18n-data-add="addStyleTitle"></h1>
|
||||
<section id="basic-info">
|
||||
<div id="basic-info-name">
|
||||
<input id="name" class="style-contributor" spellcheck="false">
|
||||
|
@ -262,7 +262,7 @@
|
|||
<input type="checkbox" id="enabled" class="style-contributor">
|
||||
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
||||
</label>
|
||||
<label id="preview-label" i18n-text="previewLabel" i18n-title="previewTooltip" class="hidden">
|
||||
<label id="preview-label" i18n-text="previewLabel" i18n-title="previewTooltip">
|
||||
<input type="checkbox" id="editor.livePreview">
|
||||
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
||||
</label>
|
||||
|
@ -392,11 +392,21 @@
|
|||
</div>
|
||||
</div>
|
||||
</details>
|
||||
<details id="integration" data-pref="editor.integration.expanded" class="ignore-pref-if-compact">
|
||||
<summary><h2 i18n-text="integration"></h2></summary>
|
||||
<details id="publish" data-pref="editor.publish.expanded" class="ignore-pref-if-compact">
|
||||
<summary><h2 i18n-text="publish"></h2></summary>
|
||||
<div>
|
||||
<button id="publish-style" i18n-text="uploadStyle"></button>
|
||||
<button id="revoke-link" i18n-text="revokeLink"></button>
|
||||
<a id="usw-url" href="https://userstyles.world" target="_blank"> </a>
|
||||
<div id="usw-link-info">
|
||||
<dl><dt i18n-text="styleName"></dt><dd data-usw="name"></dd></dl>
|
||||
<dl><dt i18n-text="genericDescription"></dt><dd data-usw="description"></dd></dl>
|
||||
</div>
|
||||
<div>
|
||||
<button id="usw-publish-style"
|
||||
i18n-data-publish="publishStyle"
|
||||
i18n-data-push="publishPush"></button>
|
||||
<button id="usw-disconnect" i18n-text="optionsSyncDisconnect"></button>
|
||||
<span id="usw-progress"></span>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
<details id="sections-list" data-pref="editor.toc.expanded" class="ignore-pref-if-compact">
|
||||
|
|
12
edit/base.js
12
edit/base.js
|
@ -21,6 +21,7 @@
|
|||
* @namespace Editor
|
||||
*/
|
||||
const editor = {
|
||||
style: null,
|
||||
dirty: DirtyReporter(),
|
||||
isUsercss: false,
|
||||
isWindowed: false,
|
||||
|
@ -34,6 +35,10 @@ const editor = {
|
|||
previewDelay: 200, // Chrome devtools uses 200
|
||||
scrollInfo: null,
|
||||
|
||||
onStyleUpdated() {
|
||||
document.documentElement.classList.toggle('is-new-style', !editor.style.id);
|
||||
},
|
||||
|
||||
updateTitle(isDirty = editor.dirty.isDirty()) {
|
||||
const {customName, name} = editor.style;
|
||||
document.title = `${
|
||||
|
@ -84,6 +89,7 @@ const baseInit = (() => {
|
|||
// switching the mode here to show the correct page ASAP, usually before DOMContentLoaded
|
||||
editor.isUsercss = Boolean(style.usercssData || !style.id && prefs.get('newStyleAsUsercss'));
|
||||
editor.style = style;
|
||||
editor.onStyleUpdated();
|
||||
editor.updateTitle(false);
|
||||
document.documentElement.classList.toggle('usercss', editor.isUsercss);
|
||||
sessionStore.justEditedStyleId = style.id || '';
|
||||
|
@ -132,8 +138,7 @@ baseInit.domReady.then(() => {
|
|||
document.body.classList.remove('compact-layout', 'fixed-header');
|
||||
window.off('scroll', fixedHeader);
|
||||
}
|
||||
for (const type of ['options', 'toc', 'lint']) {
|
||||
const el = $(`details[data-pref="editor.${type}.expanded"]`);
|
||||
for (const el of $$('details[data-pref]')) {
|
||||
el.open = compact ? false : prefs.get(el.dataset.pref);
|
||||
}
|
||||
}
|
||||
|
@ -161,9 +166,6 @@ baseInit.ready.then(() => {
|
|||
initThemeElement();
|
||||
setupLivePrefs();
|
||||
|
||||
$('#heading').textContent = t(editor.style.id ? 'editStyleHeading' : 'addStyleTitle');
|
||||
$('#preview-label').classList.toggle('hidden', !editor.style.id);
|
||||
|
||||
require(Object.values(editor.lazyKeymaps), () => {
|
||||
initKeymapElement();
|
||||
prefs.subscribe('editor.keyMap', showHotkeyInTooltip, {runNow: true});
|
||||
|
|
139
edit/edit.css
139
edit/edit.css
|
@ -7,6 +7,14 @@ body {
|
|||
font: 12px arial,sans-serif;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #000;
|
||||
transition: color .5s;
|
||||
}
|
||||
a:hover {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
#global-progress {
|
||||
position: fixed;
|
||||
height: 4px;
|
||||
|
@ -24,10 +32,17 @@ body {
|
|||
opacity: 1;
|
||||
}
|
||||
|
||||
html.is-new-style #preview-label,
|
||||
html.is-new-style #publish,
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
html.is-new-style #heading::after {
|
||||
content: attr(data-add);
|
||||
}
|
||||
html:not(.is-new-style) #heading::after {
|
||||
content: attr(data-edit);
|
||||
}
|
||||
|
||||
/************ embedded popup for simple-window editor ************/
|
||||
#popup-iframe {
|
||||
|
@ -215,7 +230,9 @@ input:invalid {
|
|||
margin-left: -13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#header summary + * {
|
||||
padding: .5rem 0;
|
||||
}
|
||||
#header summary h2 {
|
||||
display: inline-block;
|
||||
border-bottom: 1px dotted transparent;
|
||||
|
@ -225,9 +242,6 @@ input:invalid {
|
|||
padding-left: 13px; /* clicking directly on details-marker doesn't set pref so we cover it with h2 */
|
||||
}
|
||||
|
||||
#options-wrapper {
|
||||
padding: .5rem 0;
|
||||
}
|
||||
#header summary:hover h2 {
|
||||
border-color: #bbb;
|
||||
}
|
||||
|
@ -244,6 +258,7 @@ input:invalid {
|
|||
|
||||
#header details {
|
||||
margin-top: .5rem;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
#actions > * {
|
||||
|
@ -276,6 +291,81 @@ input:invalid {
|
|||
#lint:not([open]) h2 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
#publish > div > * {
|
||||
margin-top: .75em;
|
||||
}
|
||||
#publish a:visited {
|
||||
margin-top: .75em;
|
||||
}
|
||||
#publish[data-connected] summary::marker,
|
||||
#publish[data-connected] h2 {
|
||||
color: hsl(180, 100%, 20%);
|
||||
}
|
||||
#publish:not([data-connected]) #usw-link-info,
|
||||
#publish:not([data-connected]) #usw-disconnect {
|
||||
display: none;
|
||||
}
|
||||
#publish[data-connected] #usw-publish-style::after {
|
||||
content: attr(data-push);
|
||||
}
|
||||
#publish:not([data-connected]) #usw-publish-style::after {
|
||||
content: attr(data-publish);
|
||||
}
|
||||
#usw-link-info dl {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
}
|
||||
#usw-link-info dt {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
#usw-link-info dt::after {
|
||||
content: ":"
|
||||
}
|
||||
#usw-link-info dt,
|
||||
#usw-link-info dd {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
#usw-link-info dd {
|
||||
margin-left: .5em;
|
||||
}
|
||||
#usw-link-info dd[data-usw="name"] {
|
||||
font-weight: bold;
|
||||
}
|
||||
#usw-progress {
|
||||
position: relative;
|
||||
vertical-align: top;
|
||||
}
|
||||
#usw-progress .success,
|
||||
#usw-progress .unchanged {
|
||||
font-size: 150%;
|
||||
font-weight: bold;
|
||||
position: absolute;
|
||||
margin-left: .25em;
|
||||
}
|
||||
#usw-progress .success {
|
||||
margin-top: -.25em;
|
||||
}
|
||||
#usw-progress .success::after {
|
||||
content: '\2713'; /* checkmark */
|
||||
}
|
||||
#usw-progress .unchanged::after {
|
||||
content: '=';
|
||||
}
|
||||
#usw-progress .error {
|
||||
display: block;
|
||||
margin-top: .5em;
|
||||
color: red;
|
||||
}
|
||||
#usw-progress .error + div {
|
||||
font-size: smaller;
|
||||
}
|
||||
#usw-progress .lds-spinner {
|
||||
transform: scale(0.125);
|
||||
transform-origin: 0 10px;
|
||||
}
|
||||
/* options */
|
||||
#options [type="number"] {
|
||||
width: 3.5em;
|
||||
|
@ -739,7 +829,6 @@ body:not(.find-open) [data-match-highlight-count="1"] .CodeMirror-selection-high
|
|||
#lint {
|
||||
overflow: hidden;
|
||||
margin: .5rem -1rem 0;
|
||||
min-height: 30px;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
|
@ -758,7 +847,7 @@ body:not(.find-open) [data-match-highlight-count="1"] .CodeMirror-selection-high
|
|||
text-indent: -2px;
|
||||
}
|
||||
#lint > .lint-scroll-container {
|
||||
margin: 34px 10px 0;
|
||||
margin: 1rem 10px 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
|
@ -954,7 +1043,7 @@ body.linter-disabled .hidden-unless-compact {
|
|||
position: inherit;
|
||||
border-right: none;
|
||||
border-bottom: 1px dashed #AAA;
|
||||
padding: 0;
|
||||
padding: .5rem 1rem .5rem .5rem;
|
||||
}
|
||||
.fixed-header {
|
||||
padding-top: var(--fixed-padding);
|
||||
|
@ -972,24 +1061,30 @@ body.linter-disabled .hidden-unless-compact {
|
|||
.fixed-header #options {
|
||||
display: none !important;
|
||||
}
|
||||
#header summary + *,
|
||||
#lint > .lint-scroll-container {
|
||||
margin-left: 1rem;
|
||||
padding: .25rem 0 .5rem;
|
||||
}
|
||||
#actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
white-space: nowrap;
|
||||
padding: 0 1rem;
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
#header input[type="checkbox"] {
|
||||
vertical-align: middle;
|
||||
}
|
||||
#header details {
|
||||
margin: 0;
|
||||
}
|
||||
#heading,
|
||||
h2 {
|
||||
display: none;
|
||||
}
|
||||
#basic-info {
|
||||
padding: .5rem 1rem;
|
||||
margin: 0;
|
||||
margin-bottom: .5rem;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
@ -1006,22 +1101,17 @@ body.linter-disabled .hidden-unless-compact {
|
|||
#options-wrapper {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: .5rem 1rem 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
#toc {
|
||||
padding: .5rem 1rem;
|
||||
}
|
||||
#details-wrapper {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
padding-bottom: .25rem;
|
||||
}
|
||||
#options {
|
||||
#options[open] {
|
||||
width: 100%;
|
||||
}
|
||||
#sections-list[open] {
|
||||
height: 102px;
|
||||
max-height: 102px;
|
||||
}
|
||||
#sections-list[open] #toc {
|
||||
max-height: 60px;
|
||||
|
@ -1029,13 +1119,16 @@ body.linter-disabled .hidden-unless-compact {
|
|||
}
|
||||
#sections-list,
|
||||
#lint {
|
||||
width: 50%;
|
||||
max-width: 50%;
|
||||
}
|
||||
.options-column {
|
||||
flex-grow: 1;
|
||||
padding-right: .5rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.options-column > .usercss-only {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
#options-wrapper .options-column:nth-child(2) {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
@ -1054,8 +1147,9 @@ body.linter-disabled .hidden-unless-compact {
|
|||
margin-left: 0;
|
||||
padding-left: 4px;
|
||||
}
|
||||
#options h2 {
|
||||
margin: 0 0 .5em;
|
||||
#header summary h2 {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.option label {
|
||||
margin: 0;
|
||||
|
@ -1069,7 +1163,8 @@ body.linter-disabled .hidden-unless-compact {
|
|||
top: 0.2rem;
|
||||
}
|
||||
#lint > .lint-scroll-container {
|
||||
margin: 26px 1rem 0;
|
||||
padding-top: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
#lint {
|
||||
padding: 0;
|
||||
|
|
40
edit/edit.js
40
edit/edit.js
|
@ -11,7 +11,6 @@
|
|||
/* global linterMan */
|
||||
/* global prefs */
|
||||
/* global t */// localization.js
|
||||
/* global updateUI revokeLinking publishStyle */// usw-integration.js
|
||||
'use strict';
|
||||
|
||||
//#region init
|
||||
|
@ -19,7 +18,6 @@
|
|||
baseInit.ready.then(async () => {
|
||||
await waitForSheet();
|
||||
(editor.isUsercss ? SourceEditor : SectionsEditor)();
|
||||
updateUI();
|
||||
await editor.ready;
|
||||
editor.ready = true;
|
||||
editor.dirty.onChange(editor.updateDirty);
|
||||
|
@ -48,33 +46,29 @@ baseInit.ready.then(async () => {
|
|||
require(['/edit/linter-dialogs'], () => linterMan.showLintConfig());
|
||||
$('#lint-help').onclick = () =>
|
||||
require(['/edit/linter-dialogs'], () => linterMan.showLintHelp());
|
||||
$('#revoke-link').onclick = () => revokeLinking();
|
||||
$('#publish-style').onclick = () => publishStyle();
|
||||
require([
|
||||
'/edit/autocomplete',
|
||||
'/edit/global-search',
|
||||
]);
|
||||
});
|
||||
|
||||
//#endregion
|
||||
//#region events
|
||||
|
||||
const IGNORE_UPDATE_REASONS = [
|
||||
'editPreview',
|
||||
'editPreviewEnd',
|
||||
'editSave',
|
||||
'config',
|
||||
];
|
||||
|
||||
msg.onExtension(request => {
|
||||
const {style} = request;
|
||||
switch (request.method) {
|
||||
case 'styleUpdated':
|
||||
if (editor.style.id === style.id) {
|
||||
if (!['editPreview', 'editPreviewEnd', 'editSave', 'config'].includes(request.reason)) {
|
||||
if (editor.style.id === style.id && !IGNORE_UPDATE_REASONS.includes(request.reason)) {
|
||||
Promise.resolve(request.codeIsUpdated === false ? style : API.styles.get(style.id))
|
||||
.then(newStyle => {
|
||||
editor.replaceStyle(newStyle, request.codeIsUpdated);
|
||||
|
||||
if (['success-publishing', 'success-revoke'].includes(request.reason)) {
|
||||
updateUI(newStyle);
|
||||
}
|
||||
if (request.reason === 'publishing-failed') {
|
||||
messageBoxProxy.alert(newStyle._usw.publishingError, 'pre',
|
||||
'UserStyles.world: ' + t('genericError'));
|
||||
}
|
||||
});
|
||||
}
|
||||
.then(newStyle => editor.replaceStyle(newStyle, request.codeIsUpdated));
|
||||
}
|
||||
break;
|
||||
case 'styleDeleted':
|
||||
|
@ -262,15 +256,11 @@ editor.livePreview = (() => {
|
|||
|
||||
/**
|
||||
* @param {Function} [fn] - preprocessor
|
||||
* @param {boolean} [show]
|
||||
*/
|
||||
init(fn, show) {
|
||||
init(fn) {
|
||||
preprocess = fn;
|
||||
if (show != null) toggle(show);
|
||||
},
|
||||
|
||||
toggle,
|
||||
|
||||
update(newData) {
|
||||
data = newData;
|
||||
if (!port) {
|
||||
|
@ -290,10 +280,6 @@ editor.livePreview = (() => {
|
|||
});
|
||||
}
|
||||
|
||||
function toggle(state) {
|
||||
$('#preview-label').classList.toggle('hidden', !state);
|
||||
}
|
||||
|
||||
async function updatePreviewer(data) {
|
||||
const errorContainer = $('#preview-errors');
|
||||
try {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* global $ $$ $create $remove focusAccessibility */// dom.js
|
||||
/* global $ $$ $create $remove focusAccessibility toggleDataset */// dom.js
|
||||
/* global CodeMirror */
|
||||
/* global chromeLocal */// storage-util.js
|
||||
/* global colorMimicry */
|
||||
|
@ -876,15 +876,6 @@
|
|||
}
|
||||
|
||||
|
||||
function toggleDataset(el, prop, state) {
|
||||
if (state) {
|
||||
el.dataset[prop] = '';
|
||||
} else {
|
||||
delete el.dataset[prop];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function saveWindowScrollPos() {
|
||||
state.scrollX = window.scrollX;
|
||||
state.scrollY = window.scrollY;
|
||||
|
|
|
@ -25,7 +25,7 @@ function SectionsEditor() {
|
|||
|
||||
updateHeader();
|
||||
rerouteHotkeys.toggle(true); // enabled initially because we don't always focus a CodeMirror
|
||||
editor.livePreview.init(null, style.id);
|
||||
editor.livePreview.init();
|
||||
container.classList.add('section-editor');
|
||||
$('#to-mozilla').on('click', showMozillaFormat);
|
||||
$('#to-mozilla-help').on('click', showToMozillaHelp);
|
||||
|
@ -54,6 +54,11 @@ function SectionsEditor() {
|
|||
return `${t('sectionCode')} ${index + 1}`;
|
||||
},
|
||||
|
||||
getValue(asObject) {
|
||||
const st = getModel();
|
||||
return asObject ? st : MozDocMapper.styleToCss(st);
|
||||
},
|
||||
|
||||
getSearchableInputs(cm) {
|
||||
const sec = sections.find(s => s.cm === cm);
|
||||
return sec ? sec.appliesTo.map(a => a.valueEl).filter(Boolean) : [];
|
||||
|
@ -86,14 +91,13 @@ function SectionsEditor() {
|
|||
await initSections(newStyle.sections, {replace: true});
|
||||
}
|
||||
Object.assign(style, newStyle);
|
||||
editor.onStyleUpdated();
|
||||
updateHeader();
|
||||
dirty.clear();
|
||||
// Go from new style URL to edit style URL
|
||||
if (location.href.indexOf('id=') === -1 && style.id) {
|
||||
history.replaceState({}, document.title, 'edit.html?id=' + style.id);
|
||||
$('#heading').textContent = t('editStyleHeading');
|
||||
if (style.id && !/[&?]id=/.test(location.search)) {
|
||||
history.replaceState({}, document.title, `${location.pathname}?id=${style.id}`);
|
||||
}
|
||||
editor.livePreview.toggle(Boolean(style.id));
|
||||
updateLivePreview();
|
||||
},
|
||||
|
||||
|
@ -323,7 +327,7 @@ function SectionsEditor() {
|
|||
|
||||
function showMozillaFormat() {
|
||||
const popup = showCodeMirrorPopup(t('styleToMozillaFormatTitle'), '', {readOnly: true});
|
||||
popup.codebox.setValue(MozDocMapper.styleToCss(getModel()));
|
||||
popup.codebox.setValue(editor.getValue());
|
||||
popup.codebox.execCommand('selectAll');
|
||||
}
|
||||
|
||||
|
@ -425,7 +429,7 @@ function SectionsEditor() {
|
|||
editor.updateToc();
|
||||
}
|
||||
|
||||
/** @returns {Style} */
|
||||
/** @returns {StyleObj} */
|
||||
function getModel() {
|
||||
return Object.assign({}, style, {
|
||||
sections: sections.filter(s => !s.removed).map(s => s.getModel()),
|
||||
|
|
|
@ -30,7 +30,7 @@ function SourceEditor() {
|
|||
const cm = cmFactory.create($('.single-editor'));
|
||||
const sectionFinder = MozSectionFinder(cm);
|
||||
const sectionWidget = MozSectionWidget(cm, sectionFinder);
|
||||
editor.livePreview.init(preprocess, style.id);
|
||||
editor.livePreview.init(preprocess);
|
||||
createMetaCompiler(meta => {
|
||||
style.usercssData = meta;
|
||||
style.name = meta.name;
|
||||
|
@ -48,6 +48,7 @@ function SourceEditor() {
|
|||
closestVisible: () => cm,
|
||||
getEditors: () => [cm],
|
||||
getEditorTitle: () => '',
|
||||
getValue: () => cm.getValue(),
|
||||
getSearchableInputs: () => [],
|
||||
prevEditor: nextPrevSection.bind(null, -1),
|
||||
nextEditor: nextPrevSection.bind(null, 1),
|
||||
|
@ -241,9 +242,8 @@ function SourceEditor() {
|
|||
}
|
||||
sessionStore.justEditedStyleId = newStyle.id;
|
||||
Object.assign(style, newStyle);
|
||||
$('#preview-label').classList.remove('hidden');
|
||||
editor.onStyleUpdated();
|
||||
updateMeta();
|
||||
editor.livePreview.toggle(Boolean(style.id));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,49 +1,97 @@
|
|||
/* global $ $create $remove */// dom.js
|
||||
/* global $ $create $remove messageBoxProxy showSpinner toggleDataset */// dom.js
|
||||
/* global API msg */// msg.js
|
||||
/* global URLS */// toolbox.js
|
||||
/* global baseInit */
|
||||
/* global editor */
|
||||
|
||||
/* global t */// localization.js
|
||||
'use strict';
|
||||
|
||||
let uswPort;
|
||||
(() => {
|
||||
//#region Main
|
||||
|
||||
function connectToPort() {
|
||||
if (!uswPort) {
|
||||
uswPort = chrome.runtime.connect({name: 'link-style-usw'});
|
||||
uswPort.onDisconnect.addListener(err => {
|
||||
throw err;
|
||||
const ERROR_TITLE = 'UserStyles.world ' + t('genericError');
|
||||
const PROGRESS = '#usw-progress';
|
||||
let spinnerTimer = 0;
|
||||
let prevCode = '';
|
||||
|
||||
msg.onExtension(request => {
|
||||
if (request.method === 'uswData' &&
|
||||
request.style.id === editor.style.id) {
|
||||
Object.assign(editor.style, request.style);
|
||||
updateUI();
|
||||
}
|
||||
});
|
||||
|
||||
baseInit.ready.then(() => {
|
||||
updateUI();
|
||||
$('#usw-publish-style').onclick = disableWhileActive(publishStyle);
|
||||
$('#usw-disconnect').onclick = disableWhileActive(disconnect);
|
||||
});
|
||||
|
||||
async function publishStyle() {
|
||||
const {id} = editor.style;
|
||||
if (await API.data.has('usw' + id) &&
|
||||
!await messageBoxProxy.confirm(t('publishRetry'), 'danger', ERROR_TITLE)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* exported revokeLinking */
|
||||
function revokeLinking() {
|
||||
connectToPort();
|
||||
|
||||
uswPort.postMessage({reason: 'revoke', data: editor.style});
|
||||
}
|
||||
|
||||
/* exported publishStyle */
|
||||
function publishStyle() {
|
||||
connectToPort();
|
||||
const data = Object.assign(editor.style, {sourceCode: editor.getEditors()[0].getValue()});
|
||||
uswPort.postMessage({reason: 'publish', data});
|
||||
}
|
||||
|
||||
|
||||
/* exported updateUI */
|
||||
function updateUI(useStyle) {
|
||||
const style = useStyle || editor.style;
|
||||
if (style._usw && style._usw.token) {
|
||||
$('#revoke-link').style = '';
|
||||
|
||||
const linkInformation = $create('div', {id: 'link-info'}, [
|
||||
$create('p', `Style name: ${style._usw.name}`),
|
||||
$create('p', `Description: ${style._usw.description}`),
|
||||
const code = editor.getValue();
|
||||
const isDiff = code !== prevCode;
|
||||
const res = isDiff ? await API.usw.publish(id, code) : t('importReportUnchanged');
|
||||
const title = `${new Date().toLocaleString()}\n${res}`;
|
||||
const failed = /^Error:/.test(res);
|
||||
$(PROGRESS).append(...failed && [
|
||||
$create('div.error', {title}, res),
|
||||
$create('div', t('publishReconnect')),
|
||||
] || [
|
||||
$create(`span.${isDiff ? 'success' : 'unchanged'}`, {title}),
|
||||
]);
|
||||
$remove('#link-info');
|
||||
$('#integration').insertBefore(linkInformation, $('#integration').firstChild);
|
||||
} else {
|
||||
$('#revoke-link').style = 'display: none;';
|
||||
$remove('#link-info');
|
||||
if (!failed) prevCode = code;
|
||||
}
|
||||
|
||||
async function disconnect() {
|
||||
await API.usw.revoke(editor.style.id);
|
||||
prevCode = null; // to allow the next publishStyle to upload style
|
||||
}
|
||||
|
||||
function updateUI(style = editor.style) {
|
||||
const usw = style._usw || {};
|
||||
const section = $('#publish');
|
||||
toggleDataset(section, 'connected', usw.token);
|
||||
for (const type of ['name', 'description']) {
|
||||
const el = $(`dd[data-usw="${type}"]`, section);
|
||||
el.textContent = el.title = usw[type] || '';
|
||||
}
|
||||
const elUrl = $('#usw-url');
|
||||
elUrl.href = `${URLS.usw}${usw.id ? `style/${usw.id}` : ''}`;
|
||||
elUrl.textContent = t('publishUsw').replace(/<(.+)>/, `$1${usw.id ? `#${usw.id}` : ''}`);
|
||||
}
|
||||
|
||||
//#endregion
|
||||
//#region Utility
|
||||
|
||||
function disableWhileActive(fn) {
|
||||
/** @this {Element} */
|
||||
return async function () {
|
||||
this.disabled = true;
|
||||
timerOn();
|
||||
await fn().catch(console.error);
|
||||
timerOff();
|
||||
this.disabled = false;
|
||||
};
|
||||
}
|
||||
|
||||
function timerOn() {
|
||||
if (!spinnerTimer) {
|
||||
$(PROGRESS).textContent = '';
|
||||
spinnerTimer = setTimeout(showSpinner, 250, PROGRESS);
|
||||
}
|
||||
}
|
||||
|
||||
function timerOff() {
|
||||
$remove(`${PROGRESS} .lds-spinner`);
|
||||
clearTimeout(spinnerTimer);
|
||||
spinnerTimer = 0;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
})();
|
||||
|
|
|
@ -236,6 +236,11 @@ select[disabled] + .select-arrow {
|
|||
fill: hsl(0, 0%, 50%);
|
||||
}
|
||||
|
||||
summary {
|
||||
-moz-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* global stuff we use everywhere */
|
||||
.hidden {
|
||||
display: none !important;
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
<script src="content/style-injector.js"></script>
|
||||
<script src="content/apply.js"></script>
|
||||
|
||||
<link href="spinner.css" rel="stylesheet">
|
||||
<link href="install-usercss/install-usercss.css" rel="stylesheet">
|
||||
</head>
|
||||
<body id="stylus-install-usercss">
|
||||
|
|
|
@ -297,93 +297,12 @@ label {
|
|||
padding-left: 16px;
|
||||
position: relative;
|
||||
}
|
||||
/* spinner: https://github.com/loadingio/css-spinner */
|
||||
|
||||
@keyframes lds-spinner {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
.lds-spinner {
|
||||
position: absolute;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
top: 50px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: auto;
|
||||
opacity: .2;
|
||||
transition: opacity .5s;
|
||||
}
|
||||
.lds-spinner div {
|
||||
left: 94px;
|
||||
top: 23px;
|
||||
position: absolute;
|
||||
animation: lds-spinner linear 1s infinite;
|
||||
background: currentColor;
|
||||
width: 12px;
|
||||
height: 34px;
|
||||
border-radius: 20%;
|
||||
transform-origin: 6px 77px;
|
||||
}
|
||||
.lds-spinner div:nth-child(1) {
|
||||
transform: rotate(0deg);
|
||||
animation-delay: -0.916666666666667s;
|
||||
}
|
||||
.lds-spinner div:nth-child(2) {
|
||||
transform: rotate(30deg);
|
||||
animation-delay: -0.833333333333333s;
|
||||
}
|
||||
.lds-spinner div:nth-child(3) {
|
||||
transform: rotate(60deg);
|
||||
animation-delay: -0.75s;
|
||||
}
|
||||
.lds-spinner div:nth-child(4) {
|
||||
transform: rotate(90deg);
|
||||
animation-delay: -0.666666666666667s;
|
||||
}
|
||||
.lds-spinner div:nth-child(5) {
|
||||
transform: rotate(120deg);
|
||||
animation-delay: -0.583333333333333s;
|
||||
}
|
||||
.lds-spinner div:nth-child(6) {
|
||||
transform: rotate(150deg);
|
||||
animation-delay: -0.5s;
|
||||
}
|
||||
.lds-spinner div:nth-child(7) {
|
||||
transform: rotate(180deg);
|
||||
animation-delay: -0.416666666666667s;
|
||||
}
|
||||
.lds-spinner div:nth-child(8) {
|
||||
transform: rotate(210deg);
|
||||
animation-delay: -0.333333333333333s;
|
||||
}
|
||||
.lds-spinner div:nth-child(9) {
|
||||
transform: rotate(240deg);
|
||||
animation-delay: -0.25s;
|
||||
}
|
||||
.lds-spinner div:nth-child(10) {
|
||||
transform: rotate(270deg);
|
||||
animation-delay: -0.166666666666667s;
|
||||
}
|
||||
.lds-spinner div:nth-child(11) {
|
||||
transform: rotate(300deg);
|
||||
animation-delay: -0.083333333333333s;
|
||||
}
|
||||
.lds-spinner div:nth-child(12) {
|
||||
transform: rotate(330deg);
|
||||
animation-delay: 0s;
|
||||
}
|
||||
@keyframes load3 {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
animation: none;
|
||||
}
|
||||
|
||||
/************ reponsive layouts ************/
|
||||
|
|
|
@ -22,7 +22,7 @@ document.on('visibilitychange', () => {
|
|||
});
|
||||
|
||||
setTimeout(() => {
|
||||
if (!installed) {
|
||||
if (!cm) {
|
||||
$('#header').appendChild($create('.lds-spinner',
|
||||
new Array(12).fill($create('div')).map(e => e.cloneNode())));
|
||||
}
|
||||
|
|
18
js/dom.js
18
js/dom.js
|
@ -13,6 +13,8 @@
|
|||
moveFocus
|
||||
scrollElementIntoView
|
||||
setupLivePrefs
|
||||
showSpinner
|
||||
toggleDataset
|
||||
waitForSheet
|
||||
*/
|
||||
|
||||
|
@ -325,6 +327,22 @@ function setupLivePrefs(ids = prefs.knownKeys.filter(id => $('#' + id))) {
|
|||
}
|
||||
}
|
||||
|
||||
/** @param {string|Node} parent - selector or DOM node */
|
||||
async function showSpinner(parent) {
|
||||
await require(['/spinner.css']);
|
||||
parent = parent instanceof Node ? parent : $(parent);
|
||||
parent.appendChild($create('.lds-spinner',
|
||||
new Array(12).fill($create('div')).map(e => e.cloneNode())));
|
||||
}
|
||||
|
||||
function toggleDataset(el, prop, state) {
|
||||
if (state) {
|
||||
el.dataset[prop] = '';
|
||||
} else {
|
||||
delete el.dataset[prop];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector - beware of $ quirks with `#dotted.id` that won't work with $$
|
||||
* @param {Object} [opt]
|
||||
|
|
|
@ -61,7 +61,7 @@
|
|||
'editor.toc.expanded': true, // UI element state: expanded/collapsed
|
||||
'editor.options.expanded': true, // UI element state: expanded/collapsed
|
||||
'editor.lint.expanded': true, // UI element state: expanded/collapsed
|
||||
'editor.integration.expanded': true, // UI element state expanded/collapsed
|
||||
'editor.publish.expanded': true, // UI element state expanded/collapsed
|
||||
'editor.lineWrapping': true, // word wrap
|
||||
'editor.smartIndent': true, // 'smart' indent
|
||||
'editor.indentWithTabs': false, // smart indent with tabs
|
||||
|
|
|
@ -44,10 +44,10 @@
|
|||
"background/sync-manager.js",
|
||||
"background/tab-manager.js",
|
||||
"background/token-manager.js",
|
||||
"background/usw-api.js",
|
||||
"background/update-manager.js",
|
||||
"background/usercss-install-helper.js",
|
||||
"background/usercss-manager.js",
|
||||
"background/usw-api.js",
|
||||
|
||||
"background/style-manager.js",
|
||||
"background/background.js"
|
||||
|
|
|
@ -282,102 +282,3 @@ body.search-results-shown {
|
|||
margin-right: .5em;
|
||||
flex: 1 1 0;
|
||||
}
|
||||
|
||||
/* spinner: https://github.com/loadingio/css-spinner */
|
||||
.lds-spinner {
|
||||
-moz-user-select: none;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 200px; /* don't change! use "transform: scale(.75)" */
|
||||
height: 200px; /* don't change! use "transform: scale(.75)" */
|
||||
margin: auto;
|
||||
animation: lds-spinner 1s reverse;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
@keyframes lds-spinner {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.lds-spinner div {
|
||||
left: 94px;
|
||||
top: 23px;
|
||||
position: absolute;
|
||||
animation: lds-spinner linear 1s infinite;
|
||||
animation-direction: reverse;
|
||||
background: currentColor;
|
||||
width: 12px;
|
||||
height: 34px;
|
||||
border-radius: 20%;
|
||||
transform-origin: 6px 77px;
|
||||
}
|
||||
|
||||
.lds-spinner div:nth-child(1) {
|
||||
transform: rotate(0deg);
|
||||
animation-delay: -0.916666666666667s;
|
||||
}
|
||||
|
||||
.lds-spinner div:nth-child(2) {
|
||||
transform: rotate(30deg);
|
||||
animation-delay: -0.833333333333333s;
|
||||
}
|
||||
|
||||
.lds-spinner div:nth-child(3) {
|
||||
transform: rotate(60deg);
|
||||
animation-delay: -0.75s;
|
||||
}
|
||||
|
||||
.lds-spinner div:nth-child(4) {
|
||||
transform: rotate(90deg);
|
||||
animation-delay: -0.666666666666667s;
|
||||
}
|
||||
|
||||
.lds-spinner div:nth-child(5) {
|
||||
transform: rotate(120deg);
|
||||
animation-delay: -0.583333333333333s;
|
||||
}
|
||||
|
||||
.lds-spinner div:nth-child(6) {
|
||||
transform: rotate(150deg);
|
||||
animation-delay: -0.5s;
|
||||
}
|
||||
|
||||
.lds-spinner div:nth-child(7) {
|
||||
transform: rotate(180deg);
|
||||
animation-delay: -0.416666666666667s;
|
||||
}
|
||||
|
||||
.lds-spinner div:nth-child(8) {
|
||||
transform: rotate(210deg);
|
||||
animation-delay: -0.333333333333333s;
|
||||
}
|
||||
|
||||
.lds-spinner div:nth-child(9) {
|
||||
transform: rotate(240deg);
|
||||
animation-delay: -0.25s;
|
||||
}
|
||||
|
||||
.lds-spinner div:nth-child(10) {
|
||||
transform: rotate(270deg);
|
||||
animation-delay: -0.166666666666667s;
|
||||
}
|
||||
|
||||
.lds-spinner div:nth-child(11) {
|
||||
transform: rotate(300deg);
|
||||
animation-delay: -0.083333333333333s;
|
||||
}
|
||||
|
||||
.lds-spinner div:nth-child(12) {
|
||||
transform: rotate(330deg);
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* global $ $$ $create $remove */// dom.js
|
||||
/* global $ $$ $create $remove showSpinner */// dom.js
|
||||
/* global $entry tabURL */// popup.js
|
||||
/* global API */// msg.js
|
||||
/* global Events */
|
||||
|
@ -153,13 +153,6 @@
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
function showSpinner(parent) {
|
||||
parent = parent instanceof Node ? parent : $(parent);
|
||||
parent.appendChild($create('.lds-spinner',
|
||||
new Array(12).fill($create('div')).map(e => e.cloneNode())));
|
||||
}
|
||||
|
||||
function next() {
|
||||
displayedPage = Math.min(totalPages, displayedPage + 1);
|
||||
scrollToFirstResult = true;
|
||||
|
|
98
spinner.css
Normal file
98
spinner.css
Normal file
|
@ -0,0 +1,98 @@
|
|||
/* spinner: https://github.com/loadingio/css-spinner */
|
||||
.lds-spinner {
|
||||
-moz-user-select: none;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 200px; /* don't change! use "transform: scale(.75)" */
|
||||
height: 200px; /* don't change! use "transform: scale(.75)" */
|
||||
margin: auto;
|
||||
animation: lds-spinner 1s reverse;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
@keyframes lds-spinner {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.lds-spinner div {
|
||||
left: 94px;
|
||||
top: 23px;
|
||||
position: absolute;
|
||||
animation: lds-spinner linear 1s infinite;
|
||||
animation-direction: reverse;
|
||||
background: currentColor;
|
||||
width: 12px;
|
||||
height: 34px;
|
||||
border-radius: 20%;
|
||||
transform-origin: 6px 77px;
|
||||
}
|
||||
|
||||
.lds-spinner div:nth-child(1) {
|
||||
transform: rotate(0deg);
|
||||
animation-delay: -0.916666666666667s;
|
||||
}
|
||||
|
||||
.lds-spinner div:nth-child(2) {
|
||||
transform: rotate(30deg);
|
||||
animation-delay: -0.833333333333333s;
|
||||
}
|
||||
|
||||
.lds-spinner div:nth-child(3) {
|
||||
transform: rotate(60deg);
|
||||
animation-delay: -0.75s;
|
||||
}
|
||||
|
||||
.lds-spinner div:nth-child(4) {
|
||||
transform: rotate(90deg);
|
||||
animation-delay: -0.666666666666667s;
|
||||
}
|
||||
|
||||
.lds-spinner div:nth-child(5) {
|
||||
transform: rotate(120deg);
|
||||
animation-delay: -0.583333333333333s;
|
||||
}
|
||||
|
||||
.lds-spinner div:nth-child(6) {
|
||||
transform: rotate(150deg);
|
||||
animation-delay: -0.5s;
|
||||
}
|
||||
|
||||
.lds-spinner div:nth-child(7) {
|
||||
transform: rotate(180deg);
|
||||
animation-delay: -0.416666666666667s;
|
||||
}
|
||||
|
||||
.lds-spinner div:nth-child(8) {
|
||||
transform: rotate(210deg);
|
||||
animation-delay: -0.333333333333333s;
|
||||
}
|
||||
|
||||
.lds-spinner div:nth-child(9) {
|
||||
transform: rotate(240deg);
|
||||
animation-delay: -0.25s;
|
||||
}
|
||||
|
||||
.lds-spinner div:nth-child(10) {
|
||||
transform: rotate(270deg);
|
||||
animation-delay: -0.166666666666667s;
|
||||
}
|
||||
|
||||
.lds-spinner div:nth-child(11) {
|
||||
transform: rotate(300deg);
|
||||
animation-delay: -0.083333333333333s;
|
||||
}
|
||||
|
||||
.lds-spinner div:nth-child(12) {
|
||||
transform: rotate(330deg);
|
||||
animation-delay: 0s;
|
||||
}
|
Loading…
Reference in New Issue
Block a user