diff --git a/_locales/en/messages.json b/_locales/en/messages.json
index d735ab85..c00f63e5 100644
--- a/_locales/en/messages.json
+++ b/_locales/en/messages.json
@@ -1365,6 +1365,18 @@
"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"
diff --git a/background/style-manager.js b/background/style-manager.js
index be99dccf..f390bfdc 100644
--- a/background/style-manager.js
+++ b/background/style-manager.js
@@ -6,6 +6,8 @@
/* global prefs */
/* global tabMan */
/* global usercssMan */
+/* global tokenMan */
+/* global retrieveStyleInformation uploadStyle */// usw-api.js
'use strict';
/*
@@ -54,12 +56,14 @@ const styleMan = (() => {
name: style => `ID: ${style.id}`,
_id: () => uuidv4(),
_rev: () => Date.now(),
+ _usw: () => ({}),
};
const DELETE_IF_NULL = ['id', 'customName', 'md5Url', 'originalMd5'];
/** @type {Promise|boolean} will be `true` to avoid wasting a microtask tick on each `await` */
let ready = init();
chrome.runtime.onConnect.addListener(handleLivePreview);
+ chrome.runtime.onConnect.addListener(handlePublishingUSW);
//#endregion
//#region Exports
@@ -352,6 +356,56 @@ 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), 'success-revoke', true);
+ break;
+
+ case 'publish':
+ if (!style._usw || !style._usw.token) {
+ // Ensures just the style does have the _isUswLinked property as `true`.
+ for (const {style: someStyle} of dataMap.values()) {
+ if (someStyle._id === style._id) {
+ someStyle._isUswLinked = true;
+ someStyle.sourceCode = style.sourceCode;
+ const {metadata} = await API.worker.parseUsercssMeta(style.sourceCode);
+ someStyle.metadata = metadata;
+ } else {
+ delete someStyle._isUswLinked;
+ delete someStyle.sourceCode;
+ delete someStyle.metadata;
+ }
+ handleSave(await saveStyle(someStyle), null, null, false);
+ }
+ style._usw = {
+ token: await tokenMan.getToken('userstylesworld', true, style),
+ };
+
+ delete style._isUswLinked;
+ delete style.sourceCode;
+ delete style.metadata;
+ for (const [k, v] of Object.entries(await retrieveStyleInformation(style._usw.token))) {
+ style._usw[k] = v;
+ }
+ handleSave(await saveStyle(style), 'success-publishing', true);
+ }
+ uploadStyle(style);
+ break;
+ }
+ });
+ }
+
async function addIncludeExclude(type, id, rule) {
if (ready.then) await ready;
const style = Object.assign({}, id2style(id));
@@ -427,7 +481,7 @@ const styleMan = (() => {
style.id = newId;
}
uuidIndex.set(style._id, style.id);
- API.sync.put(style._id, style._rev);
+ API.sync.put(style._id, style._rev, style._usw);
}
async function saveStyle(style) {
@@ -437,7 +491,7 @@ const styleMan = (() => {
return style;
}
- function handleSave(style, reason, codeIsUpdated) {
+ function handleSave(style, reason, codeIsUpdated, broadcast = true) {
const data = id2data(style.id);
const method = data ? 'styleUpdated' : 'styleAdded';
if (!data) {
@@ -445,7 +499,7 @@ const styleMan = (() => {
} else {
data.style = style;
}
- broadcastStyleUpdated(style, reason, method, codeIsUpdated);
+ if (broadcast) broadcastStyleUpdated(style, reason, method, codeIsUpdated);
return style;
}
diff --git a/background/token-manager.js b/background/token-manager.js
index 67cefe51..272f431e 100644
--- a/background/token-manager.js
+++ b/background/token-manager.js
@@ -1,4 +1,4 @@
-/* global FIREFOX getActiveTab waitForTabUrl */// toolbox.js
+/* global FIREFOX getActiveTab waitForTabUrl URLS */// toolbox.js
/* global chromeLocal */// storage-util.js
'use strict';
@@ -48,6 +48,15 @@ const tokenMan = (() => {
'https://' + location.hostname + '.chromiumapp.org/',
scopes: ['Files.ReadWrite.AppFolder', 'offline_access'],
},
+ userstylesworld: {
+ flow: 'code',
+ clientId: 'zeDmKhJIfJqULtcrGMsWaxRtWHEimKgS',
+ clientSecret: 'wqHsvTuThQmXmDiVvOpZxPwSIbyycNFImpAOTxjaIRqDbsXcTOqrymMJKsOMuibFaij' +
+ 'ZZAkVYTDbLkQuYFKqgpMsMlFlgwQOYHvHFbgxQHDTwwdOroYhOwFuekCwXUlk',
+ authURL: URLS.usw + 'api/oauth/authorize_style',
+ tokenURL: URLS.usw + 'api/oauth/access_token',
+ redirect_uri: 'https://gusted.xyz/callback_helper/',
+ },
};
const NETWORK_LATENCY = 30; // seconds
@@ -55,11 +64,11 @@ const tokenMan = (() => {
return {
- buildKeys(name) {
+ buildKeys(name, styleId) {
const k = {
- TOKEN: `secure/token/${name}/token`,
- EXPIRE: `secure/token/${name}/expire`,
- REFRESH: `secure/token/${name}/refresh`,
+ TOKEN: `secure/token/${name}/${styleId ? `${styleId}/` : ''}token`,
+ EXPIRE: `secure/token/${name}/${styleId ? `${styleId}/` : ''}expire`,
+ REFRESH: `secure/token/${name}/${styleId ? `${styleId}/` : ''}refresh`,
};
k.LIST = Object.values(k);
return k;
@@ -69,8 +78,8 @@ const tokenMan = (() => {
return AUTH[name].clientId;
},
- async getToken(name, interactive) {
- const k = tokenMan.buildKeys(name);
+ async getToken(name, interactive, style) {
+ const k = tokenMan.buildKeys(name, style.id);
const obj = await chromeLocal.get(k.LIST);
if (obj[k.TOKEN]) {
if (!obj[k.EXPIRE] || Date.now() < obj[k.EXPIRE]) {
@@ -83,12 +92,13 @@ const tokenMan = (() => {
if (!interactive) {
throw new Error(`Invalid token: ${name}`);
}
- return authUser(name, k, interactive);
+ const accessToken = authUser(name, k, interactive);
+ return accessToken;
},
- async revokeToken(name) {
+ async revokeToken(name, styleId) {
const provider = AUTH[name];
- const k = tokenMan.buildKeys(name);
+ const k = tokenMan.buildKeys(name, styleId);
if (provider.revoke) {
try {
const token = await chromeLocal.getValue(k.TOKEN);
@@ -177,6 +187,7 @@ const tokenMan = (() => {
grant_type: 'authorization_code',
client_id: provider.clientId,
redirect_uri: query.redirect_uri,
+ state,
};
if (provider.clientSecret) {
body.client_secret = provider.clientSecret;
diff --git a/background/usw-api.js b/background/usw-api.js
new file mode 100644
index 00000000..96e37aa7
--- /dev/null
+++ b/background/usw-api.js
@@ -0,0 +1,29 @@
+/* global URLS */ // toolbox.js
+
+'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;
+}
+
+/* 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;
+}
diff --git a/content/install-hook-userstylesworld.js b/content/install-hook-userstylesworld.js
index 23f7c0d8..d7bab17c 100644
--- a/content/install-hook-userstylesworld.js
+++ b/content/install-hook-userstylesworld.js
@@ -1,3 +1,4 @@
+/* global API */// msg.js
'use strict';
(() => {
@@ -15,6 +16,12 @@
&& allowedOrigin === event.origin
) {
sendPostMessage({type: 'usw-remove-stylus-button'});
+
+ if (location.pathname === '/api/oauth/authorize_style/new') {
+ API.styles.find({_isUswLinked: true}).then(style => {
+ sendPostMessage({type: 'usw-fill-new-style', data: style});
+ });
+ }
}
};
diff --git a/edit.html b/edit.html
index cd6bac6d..a48f96f9 100644
--- a/edit.html
+++ b/edit.html
@@ -59,6 +59,7 @@
+
@@ -391,6 +392,13 @@
+
+
+
+
+
+
+
diff --git a/edit/edit.js b/edit/edit.js
index 95439259..8fccc7ef 100644
--- a/edit/edit.js
+++ b/edit/edit.js
@@ -11,6 +11,7 @@
/* global linterMan */
/* global prefs */
/* global t */// localization.js
+/* global updateUI revokeLinking publishStyle */// usw-integration.js
'use strict';
//#region init
@@ -18,6 +19,7 @@
baseInit.ready.then(async () => {
await waitForSheet();
(editor.isUsercss ? SourceEditor : SectionsEditor)();
+ updateUI();
await editor.ready;
editor.ready = true;
editor.dirty.onChange(editor.updateDirty);
@@ -42,6 +44,8 @@ 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',
@@ -52,10 +56,17 @@ msg.onExtension(request => {
const {style} = request;
switch (request.method) {
case 'styleUpdated':
- if (editor.style.id === style.id &&
- !['editPreview', 'editPreviewEnd', 'editSave', 'config'].includes(request.reason)) {
- Promise.resolve(request.codeIsUpdated === false ? style : API.styles.get(style.id))
- .then(newStyle => editor.replaceStyle(newStyle, request.codeIsUpdated));
+ if (editor.style.id === style.id) {
+ if (!['editPreview', 'editPreviewEnd', 'editSave', 'config'].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);
+ }
+ });
+ }
}
break;
case 'styleDeleted':
diff --git a/edit/usw-integration.js b/edit/usw-integration.js
new file mode 100644
index 00000000..6e2dd072
--- /dev/null
+++ b/edit/usw-integration.js
@@ -0,0 +1,49 @@
+/* global $ $create $remove */// dom.js
+/* global editor */
+
+'use strict';
+
+let uswPort;
+
+function connectToPort() {
+ if (!uswPort) {
+ uswPort = chrome.runtime.connect({name: 'link-style-usw'});
+ uswPort.onDisconnect.addListener(err => {
+ throw err;
+ });
+ }
+}
+
+
+/* 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}`),
+ ]);
+ $remove('#link-info');
+ $('#integration').insertBefore(linkInformation, $('#integration').firstChild);
+ } else {
+ $('#revoke-link').style = 'display: none;';
+ $remove('#link-info');
+ }
+}
diff --git a/manifest.json b/manifest.json
index aafb0dcc..46f3c017 100644
--- a/manifest.json
+++ b/manifest.json
@@ -44,6 +44,7 @@
"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",