211 lines
6.8 KiB
JavaScript
211 lines
6.8 KiB
JavaScript
/* global API */// msg.js
|
|
'use strict';
|
|
|
|
// eslint-disable-next-line no-unused-expressions
|
|
/^\/styles\/(\d+)(\/([^/]*))?([?#].*)?$/.test(location.pathname) && (() => {
|
|
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 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 wiretap = isOn => window[`${isOn ? 'add' : 'remove'}EventListener`](contentEventId, onPageEvent, true);
|
|
|
|
const mo = new MutationObserver(onMutation);
|
|
const observeColors = isOn =>
|
|
isOn ? mo.observe(document.body, {subtree: true, attributes: true, attributeFilter: ['value']})
|
|
: mo.disconnect();
|
|
|
|
addEventListener(orphanEventId, orphanCheck, true);
|
|
addEventListener('click', onClick, true);
|
|
addEventListener('change', onChange);
|
|
wiretap(true);
|
|
|
|
let oldStyle, pageData, style, md5, badKeys;
|
|
Promise.all([
|
|
fetch(md5Url).then(r => r.text()),
|
|
API.styles.find({md5Url}),
|
|
document.body || new Promise(resolve => addEventListener('load', resolve, {once: true})),
|
|
]).then(async res => {
|
|
md5 = res[0];
|
|
oldStyle = res[1] ||
|
|
await API.styles.find({installationUrl: `https://uso.kkx.one/style/${usoId}`}) ||
|
|
false;
|
|
const {updateUrl, originalMd5, id} = oldStyle;
|
|
sendEvent({
|
|
type: !id
|
|
? 'styleCanBeInstalledChrome'
|
|
: /\?/.test(updateUrl) || originalMd5 && originalMd5 !== md5
|
|
? 'styleCanBeUpdatedChrome'
|
|
: 'styleAlreadyInstalledChrome',
|
|
detail: updateUrl ? {updateUrl} : null,
|
|
});
|
|
observeColors(true);
|
|
});
|
|
|
|
{
|
|
const div = document.createElement('div');
|
|
const args = [pageEventId, contentEventId, usoId, apiUrl];
|
|
div.attachShadow({mode: 'closed'})
|
|
.appendChild(document.createElement('script'))
|
|
.textContent = `(${inPageContext})(${JSON.stringify(args).slice(1, -1)})`;
|
|
document.documentElement.appendChild(div).remove();
|
|
}
|
|
|
|
async function onClick(e) {
|
|
const el = e.target.closest('#install_style_button, #update_style_button, #uninstall_style_button');
|
|
if (!el) return;
|
|
el.disabled = true;
|
|
try {
|
|
const {id} = oldStyle;
|
|
if (el.id === 'uninstall_style_button') {
|
|
oldStyle = style = false;
|
|
removeEventListener('change', onChange);
|
|
await API.styles.delete(id);
|
|
return;
|
|
}
|
|
e.stopPropagation();
|
|
if (!style) await buildStyle();
|
|
style = oldStyle = await API.usercss.install(style, {
|
|
dup: {id},
|
|
vars: getPageVars(),
|
|
});
|
|
sendEvent({type: 'styleInstalledChrome'});
|
|
fetch(getMeta('stylish-install-ping-url'));
|
|
} catch (e) {
|
|
alert(chrome.i18n.getMessage('styleInstallFailed', e.message || e));
|
|
} finally {
|
|
el.disabled = false;
|
|
}
|
|
}
|
|
|
|
function onChange({target: el}) {
|
|
if (oldStyle && el.matches('[name^="ik-"], [type=file]')) {
|
|
API.usercss.configVars(oldStyle.id, getPageVars());
|
|
}
|
|
}
|
|
|
|
function onMutation(mutations) {
|
|
for (const {target: el} of mutations) {
|
|
if (el.tagName === 'INPUT' && el.type === 'text' && /^ik-/.test(el.name) && /^#[\da-f]{6}$/.test(el.value)) {
|
|
onChange({target: el});
|
|
}
|
|
}
|
|
}
|
|
|
|
function onPageEvent(e) {
|
|
pageData = e.detail;
|
|
wiretap(false);
|
|
}
|
|
|
|
async function buildStyle() {
|
|
if (!pageData) pageData = await (await fetch(apiUrl)).json();
|
|
({style, badKeys} = await API.uso.toUsercss(pageData));
|
|
Object.assign(style, {
|
|
md5Url,
|
|
id: oldStyle.id,
|
|
originalMd5: md5,
|
|
updateUrl: apiUrl,
|
|
});
|
|
}
|
|
|
|
function getPageVars() {
|
|
const {vars} = (style || oldStyle).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 ? getDataUriFromPage(el2) : el2.value;
|
|
} else {
|
|
v.value = v.type === 'select' ? val.replace(/^ik-/, '') : val;
|
|
}
|
|
}
|
|
}
|
|
return vars;
|
|
}
|
|
|
|
function getDataUriFromPage(el) {
|
|
wiretap(true);
|
|
dispatchEvent(new MouseEvent(pageEventId, {relatedTarget: el}));
|
|
return pageData;
|
|
}
|
|
|
|
function sendEvent(e) {
|
|
/* global cloneInto */// Firefox requires cloning
|
|
document.dispatchEvent(new CustomEvent(e.type,
|
|
typeof cloneInto === 'function' ? cloneInto(e, document) : e));
|
|
}
|
|
|
|
function getMeta(name) {
|
|
const e = $(`link[rel="${name}"]`);
|
|
const url = e && e.getAttribute('href');
|
|
if (url) return url[0] === '#' ? $(url).textContent : url;
|
|
}
|
|
|
|
function orphanCheck() {
|
|
if (chrome.i18n) return true;
|
|
removeEventListener(orphanEventId, orphanCheck, true);
|
|
removeEventListener('click', onClick, true);
|
|
removeEventListener('change', onChange);
|
|
dispatchEvent(new CustomEvent(pageEventId, {detail: 'quit'}));
|
|
observeColors(false);
|
|
wiretap(false);
|
|
}
|
|
})();
|
|
|
|
function inPageContext(eventId, eventIdHost, styleId, apiUrl) {
|
|
window.isInstalled = true;
|
|
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;
|
|
CR.sendMessage = function (id, msg, opts, cb = opts) {
|
|
if (id === 'fjnbnpbmkenffdnngjfgmeleoegfcffe' &&
|
|
msg && msg.type === 'deleteStyle' &&
|
|
typeof cb === 'function') {
|
|
cb(true);
|
|
} else {
|
|
return sendMessage(...arguments);
|
|
}
|
|
};
|
|
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);
|
|
}
|
|
} catch (e) {}
|
|
return res;
|
|
};
|
|
addEventListener(eventId, function onCommand(e) {
|
|
if (e.detail === 'quit') {
|
|
removeEventListener(eventId, onCommand, true);
|
|
RP.json = origJson;
|
|
done = true;
|
|
} else if (e.relatedTarget) {
|
|
send(e.relatedTarget.uploadedData);
|
|
}
|
|
}, true);
|
|
function send(data) {
|
|
dispatchEvent(new CustomEvent(eventIdHost, {__proto: null, detail: data}));
|
|
}
|
|
}
|