fix USO site installation (#1461)
This commit is contained in:
parent
6995483ec0
commit
685bf1fa3e
|
@ -6,16 +6,9 @@
|
|||
/* global syncMan */
|
||||
/* global updateMan */
|
||||
/* global usercssMan */
|
||||
/* global usoApi */
|
||||
/* global uswApi */
|
||||
/* global
|
||||
FIREFOX
|
||||
UA
|
||||
URLS
|
||||
activateTab
|
||||
download
|
||||
findExistingTab
|
||||
openURL
|
||||
*/ // toolbox.js
|
||||
/* global FIREFOX UA activateTab findExistingTab openURL */ // toolbox.js
|
||||
/* global colorScheme */ // color-scheme.js
|
||||
'use strict';
|
||||
|
||||
|
@ -42,17 +35,12 @@ addAPI(/** @namespace API */ {
|
|||
sync: syncMan,
|
||||
updater: updateMan,
|
||||
usercss: usercssMan,
|
||||
uso: usoApi,
|
||||
usw: uswApi,
|
||||
colorScheme,
|
||||
/** @type {BackgroundWorker} */
|
||||
worker: createWorker({url: '/background/background-worker'}),
|
||||
|
||||
download(url, opts) {
|
||||
return typeof url === 'string' && url.startsWith(URLS.uso) &&
|
||||
this.sender.url.startsWith(URLS.uso) &&
|
||||
download(url, opts || {});
|
||||
},
|
||||
|
||||
/** @returns {string} */
|
||||
getTabUrlPrefix() {
|
||||
return this.sender.tab.url.match(/^([\w-]+:\/+[^/#]+)/)[1];
|
||||
|
|
|
@ -139,14 +139,16 @@ const styleMan = (() => {
|
|||
},
|
||||
|
||||
/** @returns {Promise<?StyleObj>} */
|
||||
async find(filter) {
|
||||
async find(...filters) {
|
||||
if (ready.then) await ready;
|
||||
for (const filter of filters) {
|
||||
const filterEntries = Object.entries(filter);
|
||||
for (const {style} of dataMap.values()) {
|
||||
if (filterEntries.every(([key, val]) => style[key] === val)) {
|
||||
return style;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/* global API */// msg.js
|
||||
/* global RX_META URLS debounce deepMerge download ignoreChromeError */// toolbox.js
|
||||
/* global calcStyleDigest styleJSONseemsValid styleSectionsEqual */ // sections-util.js
|
||||
/* global calcStyleDigest styleSectionsEqual */ // sections-util.js
|
||||
/* global chromeLocal */// storage-util.js
|
||||
/* global compareVersion */// cmpver.js
|
||||
/* global db */
|
||||
|
@ -23,6 +23,7 @@ const updateMan = (() => {
|
|||
ERROR_JSON: 'error: JSON is invalid',
|
||||
ERROR_VERSION: 'error: version is older than installed style',
|
||||
};
|
||||
const USO_STYLES_API = `${URLS.uso}api/v1/styles/`;
|
||||
const RH_ETAG = {responseHeaders: ['etag']}; // a hashsum of file contents
|
||||
const RX_DATE2VER = new RegExp([
|
||||
/^(\d{4})/,
|
||||
|
@ -37,6 +38,7 @@ const updateMan = (() => {
|
|||
503, // service unavailable
|
||||
429, // too many requests
|
||||
];
|
||||
let usoReferers = 0;
|
||||
let lastUpdateTime;
|
||||
let checkingAll = false;
|
||||
let logQueue = [];
|
||||
|
@ -113,12 +115,13 @@ const updateMan = (() => {
|
|||
save,
|
||||
} = opts;
|
||||
if (!id) id = style.id;
|
||||
const ucd = style.usercssData;
|
||||
const {md5Url} = style;
|
||||
let {usercssData: ucd, updateUrl} = style;
|
||||
let res, state;
|
||||
try {
|
||||
await checkIfEdited();
|
||||
res = {
|
||||
style: await (ucd ? updateUsercss : updateUSO)().then(maybeSave),
|
||||
style: await (ucd && !md5Url ? updateUsercss : updateUSO)().then(maybeSave),
|
||||
updated: true,
|
||||
};
|
||||
state = STATES.UPDATED;
|
||||
|
@ -142,76 +145,45 @@ const updateMan = (() => {
|
|||
}
|
||||
|
||||
async function updateUSO() {
|
||||
const url = URLS.makeUsoArchiveCodeUrl(style.md5Url.match(/\d+/)[0]);
|
||||
const req = await tryDownload(url, RH_ETAG).catch(() => null);
|
||||
if (req) {
|
||||
return updateToUSOArchive(url, req);
|
||||
}
|
||||
const md5 = await tryDownload(style.md5Url);
|
||||
const md5 = await tryDownload(md5Url);
|
||||
if (!md5 || md5.length !== 32) {
|
||||
return Promise.reject(STATES.ERROR_MD5);
|
||||
}
|
||||
if (md5 === style.originalMd5 && style.originalDigest && !ignoreDigest) {
|
||||
return Promise.reject(STATES.SAME_MD5);
|
||||
}
|
||||
const json = await tryDownload(style.updateUrl, {responseType: 'json'});
|
||||
if (!styleJSONseemsValid(json)) {
|
||||
return Promise.reject(STATES.ERROR_JSON);
|
||||
let varsUrl = '';
|
||||
if (!ucd) {
|
||||
ucd = {};
|
||||
varsUrl = updateUrl;
|
||||
updateUrl = style.updateUrl = `${USO_STYLES_API}${md5Url.match(/\/(\d+)/)[1]}`;
|
||||
}
|
||||
usoSpooferStart();
|
||||
let json;
|
||||
try {
|
||||
json = await tryDownload(style.updateUrl, {responseType: 'json'});
|
||||
json = await updateUsercss(json.css) ||
|
||||
(await API.uso.toUsercss(json)).style;
|
||||
if (varsUrl) await API.uso.useVarsUrl(json, varsUrl);
|
||||
} finally {
|
||||
usoSpooferStop();
|
||||
}
|
||||
// USO may not provide a correctly updated originalMd5 (#555)
|
||||
json.originalMd5 = md5;
|
||||
return json;
|
||||
}
|
||||
|
||||
async function updateToUSOArchive(url, req) {
|
||||
const m2 = await getUsoEmbeddedMeta(req.response);
|
||||
if (m2) {
|
||||
url = m2.updateUrl;
|
||||
req = await tryDownload(url, RH_ETAG);
|
||||
}
|
||||
const json = await API.usercss.buildMeta({
|
||||
id,
|
||||
etag: req.headers.etag,
|
||||
md5Url: null,
|
||||
originalMd5: null,
|
||||
sourceCode: req.response,
|
||||
updateUrl: url,
|
||||
url: URLS.extractUsoArchiveInstallUrl(url),
|
||||
});
|
||||
const varUrlValues = style.updateUrl.split('?')[1];
|
||||
const varData = json.usercssData.vars;
|
||||
if (varUrlValues && varData) {
|
||||
const IK = 'ik-';
|
||||
const IK_LEN = IK.length;
|
||||
for (let [key, val] of new URLSearchParams(varUrlValues)) {
|
||||
if (!key.startsWith(IK)) continue;
|
||||
key = key.slice(IK_LEN);
|
||||
const varDef = varData[key];
|
||||
if (!varDef) continue;
|
||||
if (varDef.options) {
|
||||
let sel = val.startsWith(IK) && getVarOptByName(varDef, val.slice(IK_LEN));
|
||||
if (!sel) {
|
||||
key += '-custom';
|
||||
sel = getVarOptByName(varDef, key + '-dropdown');
|
||||
if (sel) varData[key].value = val;
|
||||
}
|
||||
if (sel) varDef.value = sel.name;
|
||||
} else {
|
||||
varDef.value = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
return API.usercss.buildCode(json);
|
||||
}
|
||||
|
||||
async function updateUsercss() {
|
||||
async function updateUsercss(css) {
|
||||
let oldVer = ucd.version;
|
||||
let {etag: oldEtag, updateUrl} = style;
|
||||
const m2 = URLS.extractUsoArchiveId(updateUrl) && await getUsoEmbeddedMeta();
|
||||
const m2 = (css || URLS.extractUsoArchiveId(updateUrl)) &&
|
||||
await getUsoEmbeddedMeta(css);
|
||||
if (m2 && m2.updateUrl) {
|
||||
updateUrl = m2.updateUrl;
|
||||
oldVer = m2.usercssData.version || '0';
|
||||
oldEtag = '';
|
||||
} else if (css) {
|
||||
return;
|
||||
}
|
||||
if (oldEtag && oldEtag === await downloadEtag()) {
|
||||
return Promise.reject(STATES.SAME_CODE);
|
||||
|
@ -284,8 +256,7 @@ const updateMan = (() => {
|
|||
}
|
||||
|
||||
function getDateFromVer(style) {
|
||||
const m = URLS.extractUsoArchiveId(style.updateUrl) &&
|
||||
style.usercssData.version.match(RX_DATE2VER);
|
||||
const m = RX_DATE2VER.exec((style.usercssData || {}).version);
|
||||
if (m) {
|
||||
m[2]--; // month is 0-based in `Date` constructor
|
||||
return new Date(...m.slice(1)).getTime();
|
||||
|
@ -294,13 +265,10 @@ const updateMan = (() => {
|
|||
|
||||
/** UserCSS metadata may be embedded in the original USO style so let's use its updateURL */
|
||||
function getUsoEmbeddedMeta(code = style.sourceCode) {
|
||||
const m = code.includes('@updateURL') && code.replace(RX_META, '').match(RX_META);
|
||||
const isRaw = arguments[0];
|
||||
const m = code.includes('@updateURL') && (isRaw ? code : code.replace(RX_META, '')).match(RX_META);
|
||||
return m && API.usercss.buildMeta({sourceCode: m[0]}).catch(() => null);
|
||||
}
|
||||
|
||||
function getVarOptByName(varDef, name) {
|
||||
return varDef.options.find(o => o.name === name);
|
||||
}
|
||||
}
|
||||
|
||||
function schedule() {
|
||||
|
@ -349,4 +317,32 @@ const updateMan = (() => {
|
|||
logLastWriteTime = Date.now();
|
||||
logQueue = [];
|
||||
}
|
||||
|
||||
function usoSpooferStart() {
|
||||
if (++usoReferers === 1) {
|
||||
chrome.webRequest.onBeforeSendHeaders.addListener(
|
||||
usoSpoofer,
|
||||
{types: ['xmlhttprequest'], urls: [USO_STYLES_API + '*']},
|
||||
['blocking', 'requestHeaders', chrome.webRequest.OnBeforeSendHeadersOptions.EXTRA_HEADERS]
|
||||
.filter(Boolean));
|
||||
}
|
||||
}
|
||||
|
||||
function usoSpooferStop() {
|
||||
if (--usoReferers <= 0) {
|
||||
usoReferers = 0;
|
||||
chrome.webRequest.onBeforeSendHeaders.removeListener(usoSpoofer);
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {chrome.webRequest.WebResponseHeadersDetails | browser.webRequest._OnBeforeSendHeadersDetails} info */
|
||||
function usoSpoofer(info) {
|
||||
if (info.tabId < 0 && URLS.ownOrigin.startsWith(info.initiator || info.originUrl || '')) {
|
||||
const {requestHeaders: hh} = info;
|
||||
const i = (hh.findIndex(h => /^referer$/i.test(h.name)) + 1 || hh.push({})) - 1;
|
||||
hh[i].name = 'referer';
|
||||
hh[i].value = URLS.uso;
|
||||
return {requestHeaders: hh};
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
|
|
@ -12,10 +12,12 @@ const usercssMan = {
|
|||
name: null,
|
||||
}),
|
||||
|
||||
async assignVars(style, oldStyle) {
|
||||
/** `src` is a style or vars */
|
||||
async assignVars(style, src) {
|
||||
const meta = style.usercssData;
|
||||
const vars = meta.vars;
|
||||
const oldVars = (oldStyle.usercssData || {}).vars;
|
||||
const meta2 = src.usercssData;
|
||||
const {vars} = meta;
|
||||
const oldVars = meta2 ? meta2.vars : src;
|
||||
if (vars && oldVars) {
|
||||
// The type of var might be changed during the update. Set value to null if the value is invalid.
|
||||
for (const [key, v] of Object.entries(vars)) {
|
||||
|
@ -43,7 +45,7 @@ const usercssMan = {
|
|||
let log;
|
||||
if (!metaOnly) {
|
||||
if (vars || assignVars) {
|
||||
await usercssMan.assignVars(style, vars ? {usercssData: {vars}} : dup);
|
||||
await usercssMan.assignVars(style, vars || dup);
|
||||
}
|
||||
await usercssMan.buildCode(style);
|
||||
log = style.log; // extracting the non-enumerable prop, otherwise it won't survive messaging
|
||||
|
@ -137,17 +139,18 @@ const usercssMan = {
|
|||
}
|
||||
},
|
||||
|
||||
async install(style) {
|
||||
return API.styles.install(await usercssMan.parse(style));
|
||||
async install(style, opts) {
|
||||
return API.styles.install(await usercssMan.parse(style, opts));
|
||||
},
|
||||
|
||||
async parse(style) {
|
||||
async parse(style, {dup, vars} = {}) {
|
||||
style = await usercssMan.buildMeta(style);
|
||||
// preserve style.vars during update
|
||||
const dup = await usercssMan.find(style);
|
||||
if (dup) {
|
||||
if (dup || (dup = await usercssMan.find(style))) {
|
||||
style.id = dup.id;
|
||||
await usercssMan.assignVars(style, dup);
|
||||
}
|
||||
if (vars || (vars = dup)) {
|
||||
await usercssMan.assignVars(style, vars);
|
||||
}
|
||||
return usercssMan.buildCode(style);
|
||||
},
|
||||
|
|
157
background/uso-api.js
Normal file
157
background/uso-api.js
Normal file
|
@ -0,0 +1,157 @@
|
|||
/* global URLS stringAsRegExp */// toolbox.js
|
||||
/* global usercssMan */
|
||||
'use strict';
|
||||
|
||||
const usoApi = {};
|
||||
|
||||
(() => {
|
||||
const pingers = {};
|
||||
|
||||
usoApi.pingback = (usoId, delay) => {
|
||||
clearTimeout(pingers[usoId]);
|
||||
delete pingers[usoId];
|
||||
if (delay > 0) {
|
||||
return new Promise(resolve => (pingers[usoId] = setTimeout(ping, delay, usoId, resolve)));
|
||||
} else if (delay !== false) {
|
||||
return ping(usoId);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Replicating USO-Archive format
|
||||
* https://github.com/33kk/uso-archive/blob/flomaster/lib/uso.js
|
||||
* https://github.com/33kk/uso-archive/blob/flomaster/lib/converters.js
|
||||
*/
|
||||
usoApi.toUsercss = async (data, {metaOnly = true, varsUrl} = {}) => {
|
||||
const badKeys = {};
|
||||
const newKeys = [];
|
||||
const descr = JSON.stringify(data.description.trim());
|
||||
const vars = (data.style_settings || []).map(makeVar, {badKeys, newKeys}).join('');
|
||||
const sourceCode = `\
|
||||
/* ==UserStyle==
|
||||
@name ${data.name}
|
||||
@namespace USO Archive
|
||||
@version ${data.updated.replace(/-/g, '').replace(/[T:]/g, '.').slice(0, 14)}
|
||||
@description ${/^"['`]|\\/.test(descr) ? descr : descr.slice(1, -1)}
|
||||
@author ${(data.user || {}).name || '?'}
|
||||
@license ${makeLicense(data.license)}${vars ? '\n@preprocessor uso' + vars : ''}`
|
||||
.replace(/\*\//g, '*\\/') +
|
||||
`==/UserStyle== */\n${newKeys[0] ? useNewKeys(data.css, badKeys) : data.css}`;
|
||||
const {style} = await usercssMan.build({sourceCode, metaOnly});
|
||||
usoApi.useVarsUrl(style, varsUrl);
|
||||
return {style, badKeys, newKeys};
|
||||
};
|
||||
|
||||
usoApi.useVarsUrl = (style, url) => {
|
||||
if (!/\?ik-/.test(url)) {
|
||||
return;
|
||||
}
|
||||
const cfg = {badKeys: {}, newKeys: []};
|
||||
const {vars} = style.usercssData;
|
||||
if (!vars) {
|
||||
return;
|
||||
}
|
||||
for (let [key, val] of new URLSearchParams(url.split('?')[1])) {
|
||||
if (!key.startsWith('ik-')) continue;
|
||||
key = makeKey(key.slice(3), cfg);
|
||||
const v = vars[key];
|
||||
if (!v) continue;
|
||||
if (v.options) {
|
||||
let sel = val.startsWith('ik-') && optByName(v, makeKey(val.slice(3), cfg));
|
||||
if (!sel) {
|
||||
key += '-custom';
|
||||
sel = optByName(v, key + '-dropdown');
|
||||
if (sel) vars[key].value = val;
|
||||
}
|
||||
if (sel) v.value = sel.name;
|
||||
} else {
|
||||
v.value = val;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
function ping(id, resolve) {
|
||||
return fetch(`${URLS.uso}styles/install/${id}?source=stylish-ch`)
|
||||
.then(resolve);
|
||||
}
|
||||
|
||||
function makeKey(key, {badKeys, newKeys}) {
|
||||
let res = badKeys[key];
|
||||
if (!res) {
|
||||
res = key.replace(/[^-\w]/g, '-');
|
||||
res += newKeys.includes(res) ? '-' : '';
|
||||
if (key !== res) {
|
||||
badKeys[key] = res;
|
||||
newKeys.push(res);
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function makeLicense(s) {
|
||||
return !s ? 'NO-REDISTRIBUTION' :
|
||||
s === 'publicdomain' ? 'CC0-1.0' :
|
||||
s.startsWith('ccby') ? `${s.toUpperCase().match(/(..)/g).join('-')}-4.0` :
|
||||
s;
|
||||
}
|
||||
|
||||
function makeVar({
|
||||
label,
|
||||
setting_type: type,
|
||||
install_key: ik,
|
||||
style_setting_options: opts,
|
||||
}) {
|
||||
const cfg = this;
|
||||
let value, suffix;
|
||||
ik = makeKey(ik, cfg);
|
||||
label = JSON.stringify(label);
|
||||
switch (type) {
|
||||
|
||||
case 'color':
|
||||
value = opts[0].value;
|
||||
break;
|
||||
|
||||
case 'text':
|
||||
value = JSON.stringify(opts[0].value);
|
||||
break;
|
||||
|
||||
case 'image': {
|
||||
const ikCust = `${ik}-custom`;
|
||||
opts.push({
|
||||
label: 'Custom',
|
||||
install_key: `${ikCust}-dropdown`,
|
||||
value: `/*[[${ikCust}]]*/`,
|
||||
});
|
||||
suffix = `\n@advanced text ${ikCust} ${label.slice(0, -1)} (Custom)" "https://foo.com/123.jpg"`;
|
||||
type = 'dropdown';
|
||||
} // fallthrough
|
||||
|
||||
case 'dropdown':
|
||||
value = '';
|
||||
for (const o of opts) {
|
||||
const def = o.default ? '*' : '';
|
||||
const val = o.value;
|
||||
const s = ` ${makeKey(o.install_key, cfg)} ${JSON.stringify(o.label + def)} <<<EOT${
|
||||
val.includes('\n') ? '\n' : ' '}${val} EOT;\n`;
|
||||
value = def ? s + value : value + s;
|
||||
}
|
||||
value = `{\n${value}}`;
|
||||
break;
|
||||
|
||||
default:
|
||||
value = '"ERROR: unknown type"';
|
||||
}
|
||||
return `\n@advanced ${type} ${ik} ${label} ${value}${suffix || ''}`;
|
||||
}
|
||||
|
||||
function optByName(v, name) {
|
||||
return v.options.find(o => o.name === name);
|
||||
}
|
||||
|
||||
function useNewKeys(css, badKeys) {
|
||||
const rxsKeys = stringAsRegExp(Object.keys(badKeys).join('\n'), '', true).replace(/\n/g, '|');
|
||||
const rxUsoVars = new RegExp(`(/\\*\\[\\[)(${rxsKeys})(?=]]\\*/)`, 'g');
|
||||
return css.replace(rxUsoVars, (s, a, key) => a + badKeys[key]);
|
||||
}
|
||||
})();
|
|
@ -1,354 +1,277 @@
|
|||
/* global API msg */// msg.js
|
||||
/* global API */// msg.js
|
||||
'use strict';
|
||||
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
/^\/styles\/(\d+)(\/([^/]*))?([?#].*)?$/.test(location.pathname) && (() => {
|
||||
const styleId = RegExp.$1;
|
||||
/^\/styles\/(\d+)(\/([^/]*))?([?#].*)?$/.test(location.pathname) && (async () => {
|
||||
if (window.INJECTED_USO === 1) return;
|
||||
window.INJECTED_USO = 1;
|
||||
|
||||
const usoId = RegExp.$1;
|
||||
const USO = 'https://userstyles.org';
|
||||
const apiUrl = `${USO}/api/v1/styles/${usoId}`;
|
||||
const md5Url = `https://update.userstyles.org/${usoId}.md5`;
|
||||
const CLICK = {
|
||||
customize: '.customize_button',
|
||||
install: '#install_style_button',
|
||||
uninstall: '#uninstall_style_button',
|
||||
update: '#update_style_button',
|
||||
};
|
||||
const CLICK_SEL = Object.values(CLICK).join(',');
|
||||
const pageEventId = `${performance.now()}${Math.random()}`;
|
||||
const contentEventId = pageEventId + ':';
|
||||
const orphanEventId = chrome.runtime.id; // id won't be available in the orphaned script
|
||||
const $ = (sel, base = document) => base.querySelector(sel);
|
||||
const toggleListener = (isOn, ...args) => (isOn ? addEventListener : removeEventListener)(...args);
|
||||
const togglePageListener = isOn => toggleListener(isOn, contentEventId, onPageEvent, true);
|
||||
|
||||
window.dispatchEvent(new CustomEvent(chrome.runtime.id + '-install'));
|
||||
window.addEventListener(chrome.runtime.id + '-install', orphanCheck, true);
|
||||
const mo = new MutationObserver(onMutation);
|
||||
const observeColors = isOn =>
|
||||
isOn ? mo.observe(document.body, {subtree: true, attributes: true, attributeFilter: ['value']})
|
||||
: mo.disconnect();
|
||||
|
||||
document.addEventListener('stylishInstallChrome', onClick);
|
||||
document.addEventListener('stylishUpdateChrome', onClick);
|
||||
let style, dup, md5, pageData, badKeys;
|
||||
|
||||
msg.on(onMessage);
|
||||
runInPage(inPageContext, pageEventId, contentEventId, usoId, apiUrl);
|
||||
addEventListener(orphanEventId, orphanCheck, true);
|
||||
addEventListener('click', onClick, true);
|
||||
togglePageListener(true);
|
||||
|
||||
let currentMd5;
|
||||
const md5Url = getMeta('stylish-md5-url') || `https://update.userstyles.org/${styleId}.md5`;
|
||||
Promise.all([
|
||||
API.styles.find({md5Url}),
|
||||
getResource(md5Url),
|
||||
onDOMready(),
|
||||
]).then(checkUpdatability);
|
||||
[md5, dup] = await Promise.all([
|
||||
fetch(md5Url).then(r => r.text()),
|
||||
API.styles.find({md5Url}, {installationUrl: `https://uso.kkx.one/style/${usoId}`})
|
||||
.then(sendVarsToPage),
|
||||
document.body || new Promise(resolve => addEventListener('load', resolve, {once: true})),
|
||||
]);
|
||||
|
||||
document.documentElement.appendChild(
|
||||
Object.assign(document.createElement('script'), {
|
||||
textContent: `(${inPageContext})('${pageEventId}')`,
|
||||
}));
|
||||
|
||||
function onMessage(msg) {
|
||||
switch (msg.method) {
|
||||
case 'ping':
|
||||
// orphaned content script check
|
||||
return true;
|
||||
case 'openSettings':
|
||||
openSettings();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/* since we are using "stylish-code-chrome" meta key on all browsers and
|
||||
US.o does not provide "advanced settings" on this url if browser is not Chrome,
|
||||
we need to fix this URL using "stylish-update-url" meta key
|
||||
*/
|
||||
function getStyleURL() {
|
||||
const textUrl = getMeta('stylish-update-url') || '';
|
||||
const jsonUrl = getMeta('stylish-code-chrome') ||
|
||||
textUrl.replace(/styles\/(\d+)\/[^?]*/, 'styles/chrome/$1.json');
|
||||
const paramsMissing = !jsonUrl.includes('?') && textUrl.includes('?');
|
||||
return jsonUrl + (paramsMissing ? textUrl.replace(/^[^?]+/, '') : '');
|
||||
}
|
||||
|
||||
function checkUpdatability([installedStyle, md5]) {
|
||||
// TODO: remove the following statement when USO is fixed
|
||||
document.dispatchEvent(new CustomEvent(pageEventId, {
|
||||
detail: installedStyle && installedStyle.updateUrl,
|
||||
}));
|
||||
currentMd5 = md5;
|
||||
if (!installedStyle) {
|
||||
sendEvent({type: 'styleCanBeInstalledChrome'});
|
||||
return;
|
||||
}
|
||||
const isCustomizable = /\?/.test(installedStyle.updateUrl);
|
||||
const md5Url = getMeta('stylish-md5-url');
|
||||
if (md5Url && installedStyle.md5Url && installedStyle.originalMd5) {
|
||||
reportUpdatable(isCustomizable || md5 !== installedStyle.originalMd5);
|
||||
if (!dup.id) {
|
||||
sendStylishEvent('styleCanBeInstalledChrome');
|
||||
} else if (dup.originalMd5 && dup.originalMd5 !== md5 || !dup.usercssData || !dup.md5Url) {
|
||||
// allow update if 1) changed, 2) is a classic USO style, 3) is from USO-archive
|
||||
sendStylishEvent('styleCanBeUpdatedChrome');
|
||||
} else {
|
||||
getStyleJson().then(json => {
|
||||
reportUpdatable(
|
||||
isCustomizable ||
|
||||
!json ||
|
||||
!styleSectionsEqual(json, installedStyle));
|
||||
});
|
||||
sendStylishEvent('styleAlreadyInstalledChrome');
|
||||
}
|
||||
|
||||
function prepareInstallButton() {
|
||||
return new Promise(resolve => {
|
||||
const observer = new MutationObserver(check);
|
||||
observer.observe(document.documentElement, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
check();
|
||||
|
||||
function check() {
|
||||
if (document.querySelector('#install_style_button')) {
|
||||
resolve();
|
||||
observer.disconnect();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function reportUpdatable(isUpdatable) {
|
||||
prepareInstallButton().then(() => {
|
||||
sendEvent({
|
||||
type: isUpdatable
|
||||
? 'styleCanBeUpdatedChrome'
|
||||
: 'styleAlreadyInstalledChrome',
|
||||
detail: {
|
||||
updateUrl: installedStyle.updateUrl,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function sendEvent(event) {
|
||||
sendEvent.lastEvent = event;
|
||||
let {type, detail = null} = event;
|
||||
if (typeof cloneInto !== 'undefined') {
|
||||
// Firefox requires explicit cloning, however USO can't process our messages anyway
|
||||
// because USO tries to use a global "event" variable deprecated in Firefox
|
||||
detail = cloneInto({detail}, document); /* global cloneInto */
|
||||
} else {
|
||||
detail = {detail};
|
||||
}
|
||||
document.dispatchEvent(new CustomEvent(type, detail));
|
||||
}
|
||||
|
||||
function onClick(event) {
|
||||
if (onClick.processing || !orphanCheck()) {
|
||||
return;
|
||||
}
|
||||
onClick.processing = true;
|
||||
doInstall()
|
||||
.then(() => {
|
||||
if (!event.type.includes('Update')) {
|
||||
// FIXME: sometimes the button is broken i.e. the button sends
|
||||
// 'install' instead of 'update' event while the style is already
|
||||
// install.
|
||||
// This triggers an incorrect install count but we don't really care.
|
||||
return getResource(getMeta('stylish-install-ping-url-chrome'));
|
||||
}
|
||||
})
|
||||
.catch(console.error)
|
||||
.then(done);
|
||||
function done() {
|
||||
setTimeout(() => {
|
||||
onClick.processing = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function doInstall() {
|
||||
let oldStyle;
|
||||
return API.styles.find({
|
||||
md5Url: getMeta('stylish-md5-url') || location.href,
|
||||
})
|
||||
.then(_oldStyle => {
|
||||
oldStyle = _oldStyle;
|
||||
return oldStyle ?
|
||||
oldStyle.name :
|
||||
getResource(getMeta('stylish-description'));
|
||||
})
|
||||
.then(name => {
|
||||
const props = {};
|
||||
if (oldStyle) {
|
||||
props.id = oldStyle.id;
|
||||
}
|
||||
return saveStyleCode(oldStyle ? 'styleUpdate' : 'styleInstall', name, props);
|
||||
});
|
||||
}
|
||||
|
||||
async function saveStyleCode(message, name, addProps = {}) {
|
||||
const isNew = message === 'styleInstall';
|
||||
const needsConfirmation = isNew || !saveStyleCode.confirmed;
|
||||
if (needsConfirmation && !confirm(chrome.i18n.getMessage(message, [name]))) {
|
||||
return Promise.reject();
|
||||
}
|
||||
saveStyleCode.confirmed = true;
|
||||
enableUpdateButton(false);
|
||||
const json = await getStyleJson();
|
||||
if (!json) {
|
||||
prompt(chrome.i18n.getMessage('styleInstallFailed', ''),
|
||||
'https://github.com/openstyles/stylus/issues/195');
|
||||
return;
|
||||
}
|
||||
// Update originalMd5 since USO changed it (2018-11-11) to NOT match the current md5
|
||||
const style = await API.styles.install(Object.assign(json, addProps, {originalMd5: currentMd5}));
|
||||
if (!isNew && style.updateUrl.includes('?')) {
|
||||
enableUpdateButton(true);
|
||||
} else {
|
||||
sendEvent({type: 'styleInstalledChrome'});
|
||||
}
|
||||
|
||||
function enableUpdateButton(state) {
|
||||
const important = s => s.replace(/;/g, '!important;');
|
||||
const button = document.getElementById('update_style_button');
|
||||
if (button) {
|
||||
button.style.cssText = state ? '' : important('pointer-events: none; opacity: .35;');
|
||||
const icon = button.querySelector('img[src*=".svg"]');
|
||||
if (icon) {
|
||||
icon.style.cssText = state ? '' : important('transition: transform 5s; transform: rotate(0);');
|
||||
if (state) {
|
||||
setTimeout(() => (icon.style.cssText += important('transform: rotate(10turn);')));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getMeta(name) {
|
||||
const e = document.querySelector(`link[rel="${name}"]`);
|
||||
return e ? e.getAttribute('href') : null;
|
||||
}
|
||||
|
||||
async function getResource(url, opts) {
|
||||
async function onClick(e) {
|
||||
const el = e.target.closest(CLICK_SEL);
|
||||
if (!el) return;
|
||||
el.disabled = true;
|
||||
const {id} = dup;
|
||||
try {
|
||||
return url.startsWith('#')
|
||||
? document.getElementById(url.slice(1)).textContent
|
||||
: await API.download(url, opts);
|
||||
} catch (error) {
|
||||
alert('Error\n' + error.message);
|
||||
return Promise.reject(error);
|
||||
if (el.matches(CLICK.uninstall)) {
|
||||
dup = style = false;
|
||||
removeEventListener('change', onChange);
|
||||
await API.styles.delete(id);
|
||||
return;
|
||||
}
|
||||
if (el.matches(CLICK.customize)) {
|
||||
const isOn = dup && !$('#style-settings');
|
||||
toggleListener(isOn, 'change', onChange);
|
||||
observeColors(isOn);
|
||||
return;
|
||||
}
|
||||
e.stopPropagation();
|
||||
if (!style) await buildStyle();
|
||||
style = dup = await API.usercss.install(style, {
|
||||
dup: {id},
|
||||
vars: getPageVars(),
|
||||
});
|
||||
sendStylishEvent('styleInstalledChrome');
|
||||
API.uso.pingback(id);
|
||||
} catch (e) {
|
||||
alert(chrome.i18n.getMessage('styleInstallFailed', e.message || e));
|
||||
} finally {
|
||||
el.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// USO providing md5Url as "https://update.update.userstyles.org/#####.md5"
|
||||
// instead of "https://update.userstyles.org/#####.md5"
|
||||
async function getStyleJson() {
|
||||
try {
|
||||
const style = await getResource(getStyleURL(), {responseType: 'json'});
|
||||
const codeElement = document.getElementById('stylish-code');
|
||||
if (!style || !Array.isArray(style.sections) || style.sections.length ||
|
||||
codeElement && !codeElement.textContent.trim()) {
|
||||
return style;
|
||||
}
|
||||
const code = await getResource(getMeta('stylish-update-url'));
|
||||
style.sections = (await API.worker.parseMozFormat({code})).sections;
|
||||
if (style.md5Url) style.md5Url = style.md5Url.replace('update.update', 'update');
|
||||
return style;
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* The sections are checked in successive order because it matters when many sections
|
||||
* match the same URL and they have rules with the same CSS specificity
|
||||
* @param {Object} a - first style object
|
||||
* @param {Object} b - second style object
|
||||
* @returns {?boolean}
|
||||
*/
|
||||
function styleSectionsEqual({sections: a}, {sections: b}) {
|
||||
const targets = ['urls', 'urlPrefixes', 'domains', 'regexps'];
|
||||
return a && b && a.length === b.length && a.every(sameSection);
|
||||
function sameSection(secA, i) {
|
||||
return equalOrEmpty(secA.code, b[i].code, 'string', (a, b) => a === b) &&
|
||||
targets.every(target => equalOrEmpty(secA[target], b[i][target], 'array', arrayMirrors));
|
||||
}
|
||||
function equalOrEmpty(a, b, type, comparator) {
|
||||
const typeA = type === 'array' ? Array.isArray(a) : typeof a === type;
|
||||
const typeB = type === 'array' ? Array.isArray(b) : typeof b === type;
|
||||
return typeA && typeB && comparator(a, b) ||
|
||||
(a == null || typeA && !a.length) &&
|
||||
(b == null || typeB && !b.length);
|
||||
}
|
||||
function arrayMirrors(a, b) {
|
||||
return a.length === b.length &&
|
||||
a.every(el => b.includes(el)) &&
|
||||
b.every(el => a.includes(el));
|
||||
function onChange({target: el}) {
|
||||
if (dup && el.matches('[name^="ik-"], [type=file]')) {
|
||||
API.usercss.configVars(dup.id, getPageVars());
|
||||
}
|
||||
}
|
||||
|
||||
function onDOMready() {
|
||||
return document.readyState !== 'loading'
|
||||
? Promise.resolve()
|
||||
: new Promise(resolve => document.addEventListener('DOMContentLoaded', resolve, {once: true}));
|
||||
function onMutation(mutations) {
|
||||
for (const {target: el} of mutations) {
|
||||
if (el.style.display === 'none' &&
|
||||
/^ik-/.test(el.name) &&
|
||||
/^#[\da-f]{6}$/.test(el.value)) {
|
||||
onChange({target: el});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function openSettings(countdown = 10e3) {
|
||||
const button = document.querySelector('.customize_button');
|
||||
if (button) {
|
||||
button.dispatchEvent(new MouseEvent('click', {bubbles: true}));
|
||||
setTimeout(function pollArea(countdown = 2000) {
|
||||
const area = document.getElementById('advancedsettings_area');
|
||||
if (area || countdown < 0) {
|
||||
(area || button).scrollIntoView({behavior: 'smooth', block: area ? 'end' : 'center'});
|
||||
function onPageEvent(e) {
|
||||
pageData = e.detail;
|
||||
togglePageListener(false);
|
||||
}
|
||||
|
||||
async function buildStyle() {
|
||||
if (!pageData) pageData = await (await fetch(apiUrl)).json();
|
||||
({style, badKeys} = await API.uso.toUsercss(pageData, {varsUrl: dup.updateUrl}));
|
||||
Object.assign(style, {
|
||||
md5Url,
|
||||
id: dup.id,
|
||||
originalMd5: md5,
|
||||
updateUrl: apiUrl,
|
||||
});
|
||||
}
|
||||
|
||||
function getPageVars() {
|
||||
const {vars} = (style || dup).usercssData;
|
||||
for (const el of document.querySelectorAll('[name^="ik-"]')) {
|
||||
const name = el.name.slice(3); // dropping "ik-"
|
||||
const ik = badKeys[name] || name;
|
||||
const v = vars[ik] || false;
|
||||
const isImage = el.type === 'radio';
|
||||
if (v && (!isImage || el.checked)) {
|
||||
const val = el.value;
|
||||
const isFile = val === 'user-upload';
|
||||
if (isImage && (isFile || val === 'user-url')) {
|
||||
const el2 = $(`[type=${isFile ? 'file' : 'url'}]`, el.parentElement);
|
||||
const ikCust = `${ik}-custom`;
|
||||
v.value = `${ikCust}-dropdown`;
|
||||
vars[ikCust].value = isFile ? getFileUriFromPage(el2) : el2.value;
|
||||
} else {
|
||||
setTimeout(pollArea, 100, countdown - 100);
|
||||
v.value = v.type === 'select' ? val.replace(/^ik-/, '') : val;
|
||||
}
|
||||
}, 500);
|
||||
} else if (countdown > 0) {
|
||||
setTimeout(openSettings, 100, countdown - 100);
|
||||
}
|
||||
}
|
||||
return vars;
|
||||
}
|
||||
|
||||
function getFileUriFromPage(el) {
|
||||
togglePageListener(true);
|
||||
sendPageEvent(el);
|
||||
return pageData;
|
||||
}
|
||||
|
||||
function runInPage(fn, ...args) {
|
||||
const div = document.createElement('div');
|
||||
div.attachShadow({mode: 'closed'})
|
||||
.appendChild(document.createElement('script'))
|
||||
.textContent = `(${fn})(${JSON.stringify(args).slice(1, -1)})`;
|
||||
document.documentElement.appendChild(div).remove();
|
||||
}
|
||||
|
||||
function sendPageEvent(data) {
|
||||
dispatchEvent(data instanceof Node
|
||||
? new MouseEvent(pageEventId, {relatedTarget: data})
|
||||
: new CustomEvent(pageEventId, {detail: data}));
|
||||
//* global cloneInto */// WARNING! Firefox requires cloning of an object `detail`
|
||||
}
|
||||
|
||||
function sendStylishEvent(type) {
|
||||
document.dispatchEvent(new Event(type));
|
||||
}
|
||||
|
||||
function sendVarsToPage(style) {
|
||||
if (style) {
|
||||
const vars = (style.usercssData || {}).vars || `${style.updateUrl}`.split('?')[1];
|
||||
if (vars) sendPageEvent('vars:' + JSON.stringify(vars));
|
||||
}
|
||||
return style || false;
|
||||
}
|
||||
|
||||
function orphanCheck() {
|
||||
try {
|
||||
if (chrome.i18n.getUILanguage()) {
|
||||
return true;
|
||||
}
|
||||
} catch (e) {}
|
||||
// In Chrome content script is orphaned on an extension update/reload
|
||||
// so we need to detach event listeners
|
||||
window.removeEventListener(chrome.runtime.id + '-install', orphanCheck, true);
|
||||
document.removeEventListener('stylishInstallChrome', onClick);
|
||||
document.removeEventListener('stylishUpdateChrome', onClick);
|
||||
try {
|
||||
msg.off(onMessage);
|
||||
} catch (e) {}
|
||||
if (chrome.i18n) return true;
|
||||
removeEventListener(orphanEventId, orphanCheck, true);
|
||||
removeEventListener('click', onClick, true);
|
||||
removeEventListener('change', onChange);
|
||||
sendPageEvent('quit');
|
||||
observeColors(false);
|
||||
togglePageListener(false);
|
||||
}
|
||||
})();
|
||||
|
||||
function inPageContext(eventId) {
|
||||
document.currentScript.remove();
|
||||
function inPageContext(eventId, eventIdHost, styleId, apiUrl) {
|
||||
window.isInstalled = true;
|
||||
const origMethods = {
|
||||
json: Response.prototype.json,
|
||||
byId: document.getElementById,
|
||||
const {dispatchEvent, CustomEvent, removeEventListener} = window;
|
||||
const apply = Map.call.bind(Map.apply);
|
||||
const CR = chrome.runtime;
|
||||
const {sendMessage} = CR;
|
||||
const RP = Response.prototype;
|
||||
const origJson = RP.json;
|
||||
let done, vars;
|
||||
CR.sendMessage = function (id, msg, opts, cb = opts) {
|
||||
if (id === 'fjnbnpbmkenffdnngjfgmeleoegfcffe' &&
|
||||
msg && msg.type === 'deleteStyle' &&
|
||||
typeof cb === 'function') {
|
||||
cb(true);
|
||||
} else {
|
||||
return sendMessage(...arguments);
|
||||
}
|
||||
};
|
||||
let vars;
|
||||
// USO bug workaround: prevent errors in console after install and busy cursor
|
||||
document.getElementById = id =>
|
||||
origMethods.byId.call(document, id) ||
|
||||
(/^(stylish-code|stylish-installed-style-installed-\w+|post-install-ad|style-install-unknown)$/.test(id)
|
||||
? Object.assign(document.createElement('p'), {className: 'afterdownload-ad'})
|
||||
: null);
|
||||
// USO bug workaround: use the actual image data in customized settings
|
||||
document.addEventListener(eventId, ({detail}) => {
|
||||
vars = /\?/.test(detail) && new URL(detail).searchParams;
|
||||
if (!vars) Response.prototype.json = origMethods.json;
|
||||
}, {once: true});
|
||||
Response.prototype.json = async function () {
|
||||
const json = await origMethods.json.apply(this, arguments);
|
||||
if (vars && json && Array.isArray(json.style_settings)) {
|
||||
Response.prototype.json = origMethods.json;
|
||||
RP.json = async function () {
|
||||
const res = await apply(origJson, this, arguments);
|
||||
try {
|
||||
if (!done && this.url === apiUrl) {
|
||||
RP.json = origJson;
|
||||
done = true; // will be used if called by another script that saved our RP.json hook
|
||||
send(res);
|
||||
setVars(res);
|
||||
}
|
||||
} catch (e) {}
|
||||
return res;
|
||||
};
|
||||
addEventListener(eventId, onCommand, true);
|
||||
function onCommand(e) {
|
||||
if (e.detail === 'quit') {
|
||||
removeEventListener(eventId, onCommand, true);
|
||||
CR.sendMessage = sendMessage;
|
||||
RP.json = origJson;
|
||||
done = true;
|
||||
} else if (/^vars:/.test(e.detail)) {
|
||||
vars = JSON.parse(e.detail.slice(5));
|
||||
} else if (e.relatedTarget) {
|
||||
send(e.relatedTarget.uploadedData);
|
||||
}
|
||||
}
|
||||
function send(data) {
|
||||
dispatchEvent(new CustomEvent(eventIdHost, {__proto: null, detail: data}));
|
||||
}
|
||||
function setVars(json) {
|
||||
const images = new Map();
|
||||
for (const ss of json.style_settings) {
|
||||
let value = vars.get('ik-' + ss.install_key);
|
||||
if (!value || !(ss.style_setting_options || [])[0]) {
|
||||
const isNew = typeof vars === 'object';
|
||||
const badKeys = {};
|
||||
const newKeys = [];
|
||||
const makeKey = ({install_key: key}) => {
|
||||
let res = isNew ? badKeys[key] : key;
|
||||
if (!res) {
|
||||
res = key.replace(/[^-\w]/g, '-');
|
||||
res += newKeys.includes(res) ? '-' : '';
|
||||
if (key !== res) {
|
||||
badKeys[key] = res;
|
||||
newKeys.push(res);
|
||||
}
|
||||
}
|
||||
return res;
|
||||
};
|
||||
if (!isNew) vars = new URLSearchParams(vars);
|
||||
for (const ss of json.style_settings || []) {
|
||||
const ik = makeKey(ss);
|
||||
let value = isNew ? (vars[ik] || {}).value : vars.get('ik-' + ik);
|
||||
if (value == null || !(ss.style_setting_options || [])[0]) {
|
||||
continue;
|
||||
}
|
||||
if (value.startsWith('ik-')) {
|
||||
if (ss.setting_type === 'image') {
|
||||
let isListed;
|
||||
for (const opt of ss.style_setting_options) {
|
||||
isListed |= opt.default = (opt.value === value);
|
||||
}
|
||||
images.set(ik, {url: isNew && !isListed ? vars[`${ik}-custom`].value : value, isListed});
|
||||
} else if (value.startsWith('ik-') || isNew && vars[ik].type === 'select') {
|
||||
value = value.replace(/^ik-/, '');
|
||||
const def = ss.style_setting_options.find(item => item.default);
|
||||
if (!def || def.install_key !== value) {
|
||||
if (!def || makeKey(def) !== value) {
|
||||
if (def) def.default = false;
|
||||
for (const item of ss.style_setting_options) {
|
||||
if (item.install_key === value) {
|
||||
if (makeKey(item) === value) {
|
||||
item.default = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (ss.setting_type === 'image') {
|
||||
let isListed;
|
||||
for (const opt of ss.style_setting_options) {
|
||||
isListed |= opt.default = (opt.value === value);
|
||||
}
|
||||
images.set(ss.install_key, {url: value, isListed});
|
||||
} else {
|
||||
const item = ss.style_setting_options[0];
|
||||
if (item.value !== value && item.install_key === 'placeholder') {
|
||||
|
@ -356,23 +279,18 @@ function inPageContext(eventId) {
|
|||
}
|
||||
}
|
||||
}
|
||||
if (images.size) {
|
||||
if (!images.size) return;
|
||||
new MutationObserver((_, observer) => {
|
||||
if (document.getElementById('style-settings')) {
|
||||
if (!document.getElementById('style-settings')) return;
|
||||
observer.disconnect();
|
||||
for (const [name, {url, isListed}] of images) {
|
||||
const elRadio = document.querySelector(`input[name="ik-${name}"][value="user-url"]`);
|
||||
const elUrl = elRadio &&
|
||||
document.getElementById(elRadio.id.replace('url-choice', 'user-url'));
|
||||
const elUrl = elRadio && document.getElementById(elRadio.id.replace('url-choice', 'user-url'));
|
||||
if (elUrl) {
|
||||
elRadio.checked = !isListed;
|
||||
elUrl.value = url;
|
||||
}
|
||||
}
|
||||
}
|
||||
}).observe(document, {childList: true, subtree: true});
|
||||
}
|
||||
}
|
||||
return json;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -108,7 +108,8 @@ function createStyleElement({style, name: nameLC}) {
|
|||
parts.homepage.href = parts.homepage.title = style.url || '';
|
||||
parts.infoVer.textContent = ud ? ud.version : '';
|
||||
parts.infoVer.dataset.value = ud ? ud.version : '';
|
||||
if (URLS.extractUsoArchiveId(style.updateUrl)) {
|
||||
// USO-raw and USO-archive version is a date for which we show the Age column
|
||||
if (ud && (style.md5Url || URLS.extractUsoArchiveId(style.updateUrl))) {
|
||||
parts.infoVer.dataset.isDate = '';
|
||||
} else {
|
||||
delete parts.infoVer.dataset.isDate;
|
||||
|
|
|
@ -48,6 +48,7 @@
|
|||
"background/update-manager.js",
|
||||
"background/usercss-install-helper.js",
|
||||
"background/usercss-manager.js",
|
||||
"background/uso-api.js",
|
||||
"background/usw-api.js",
|
||||
|
||||
"background/style-manager.js",
|
||||
|
|
|
@ -41,7 +41,6 @@
|
|||
* --------------------- Stylus' internally added extras
|
||||
* @prop {boolean} installed
|
||||
* @prop {number} installedStyleId
|
||||
* @prop {number} pingbackTimer
|
||||
*/
|
||||
/** @type IndexEntry[] */
|
||||
let results;
|
||||
|
@ -149,7 +148,7 @@
|
|||
restoreScrollPosition();
|
||||
const result = results.find(r => r.installedStyleId === id);
|
||||
if (result) {
|
||||
clearTimeout(result.pingbackTimer);
|
||||
API.uso.pingback(result.i, false);
|
||||
renderActionButtons(result.i, -1);
|
||||
}
|
||||
});
|
||||
|
@ -437,11 +436,7 @@
|
|||
installButton.disabled = true;
|
||||
entry.style.setProperty('pointer-events', 'none', 'important');
|
||||
delete entry.dataset.error;
|
||||
if (fmt) {
|
||||
// FIXME: move this to background page and create an API like installUSOStyle
|
||||
result.pingbackTimer = setTimeout(download, PINGBACK_DELAY,
|
||||
`${URLS.uso}styles/install/${id}?source=stylish-ch`);
|
||||
}
|
||||
if (fmt) API.uso.pingback(id, PINGBACK_DELAY);
|
||||
|
||||
const updateUrl = fmt ? URLS.makeUsoArchiveCodeUrl(id) : URLS.makeUswCodeUrl(id);
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user