diff --git a/_locales/en/messages.json b/_locales/en/messages.json index f7cb74fa..d735ab85 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -630,6 +630,10 @@ "message": "Translate", "description": "Transifex link text on the manage page" }, + "linkUSW": { + "message": "Upload and discover styles on userstyles.world", + "description": "Link text for https://userstyles.world/ on the manage page" + }, "linterCSSLintIncompatible": { "message": "CSSLint doesn't support $preprocessorname$ preprocessor", "placeholders": { diff --git a/background/style-manager.js b/background/style-manager.js index ed074854..bdbf3748 100644 --- a/background/style-manager.js +++ b/background/style-manager.js @@ -223,7 +223,8 @@ const styleMan = (() => { style = mergeWithMapped(style); const url = !style.url && style.updateUrl && ( URLS.extractUsoArchiveInstallUrl(style.updateUrl) || - URLS.extractGreasyForkInstallUrl(style.updateUrl) + URLS.extractGreasyForkInstallUrl(style.updateUrl) || + URLS.extractUSwInstallUrl(style.updateUrl) ); if (url) style.url = style.installationUrl = url; style.originalDigest = await calcStyleDigest(style); diff --git a/background/usercss-install-helper.js b/background/usercss-install-helper.js index 6656ac7b..20e75536 100644 --- a/background/usercss-install-helper.js +++ b/background/usercss-install-helper.js @@ -36,6 +36,7 @@ bgReady.all.then(() => { chrome.webRequest.onBeforeSendHeaders.addListener(maybeInstallFromDistro, { urls: [ URLS.usoArchiveRaw + 'usercss/*.user.css', + URLS.usw + 'api/style/*.user.css', '*://greasyfork.org/scripts/*/code/*.user.css', '*://sleazyfork.org/scripts/*/code/*.user.css', ...[].concat( diff --git a/js/prefs.js b/js/prefs.js index 0d1879dc..3d7b93fa 100644 --- a/js/prefs.js +++ b/js/prefs.js @@ -37,6 +37,8 @@ 'popup.autoResort': false, // auto resort styles after toggling 'popup.borders': false, // add white borders on the sides 'popup.findStylesInline': true, // use the inline style search + /** @type {'n' | 'u' | 't' | 'w' | 'r'} see IndexEntry */ + 'popup.findSort': 'u', // the inline style search sort order 'manage.onlyEnabled': false, // display only enabled styles 'manage.onlyLocal': false, // display only styles created locally diff --git a/js/toolbox.js b/js/toolbox.js index 57d1ad9a..589a35e9 100644 --- a/js/toolbox.js +++ b/js/toolbox.js @@ -78,6 +78,9 @@ const URLS = { usoArchive: 'https://33kk.github.io/uso-archive/', usoArchiveRaw: 'https://raw.githubusercontent.com/33kk/uso-archive/flomaster/data/', + + usw: 'https://userstyles.world/', + extractUsoArchiveId: url => url && url.startsWith(URLS.usoArchiveRaw) && @@ -91,6 +94,16 @@ const URLS = { extractGreasyForkInstallUrl: url => /^(https:\/\/(?:greasy|sleazy)fork\.org\/scripts\/\d+)[^/]*\/code\/[^/]*\.user\.css$|$/.exec(url)[1], + extractUSwId: url => + url && + url.startsWith(URLS.usw) && + Number(url.match(/\/(\d+)\.user\.css|$/)[1]), + extractUSwInstallUrl: url => { + const id = URLS.extractUSwId(url); + return id ? `${URLS.usw}style/${id}` : ''; + }, + makeUswCodeUrl: id => `${URLS.usw}api/style/${id}.user.css`, + supported: url => ( url.startsWith('http') || url.startsWith('ftp') || diff --git a/manage.html b/manage.html index f22b5e47..f6f9ea5c 100644 --- a/manage.html +++ b/manage.html @@ -333,6 +333,9 @@ + + + diff --git a/manage/manage.css b/manage/manage.css index 88fd8083..8e66ccd3 100644 --- a/manage/manage.css +++ b/manage/manage.css @@ -440,6 +440,24 @@ a:hover { margin: 0 .5em; } +#link-usw { + display: flex; + align-items: center; + margin-top: .5em; +} + +#link-usw img { + max-width: 2.5em; + max-height: 2.5em; + margin-right: .75em; + filter: grayscale(1); + transition: filter .5s; +} + +#link-usw:hover img { + filter: none; +} + .newUI .entry .svg-icon.checked, .newUI .entry:hover .svg-icon.checked { fill: #000; diff --git a/popup/search.css b/popup/search.css index 127738b4..4887fadd 100644 --- a/popup/search.css +++ b/popup/search.css @@ -82,6 +82,12 @@ body.search-results-shown { overflow-wrap: break-word; } +.search-result-title img { + width: 20px; + height: 20px; + margin: -6px 4px -6px 0; +} + .search-result-title span { font-size: 12px; font-weight: 600; diff --git a/popup/search.js b/popup/search.js index cac23f0d..a08ea171 100644 --- a/popup/search.js +++ b/popup/search.js @@ -12,6 +12,11 @@ const RESULT_ID_PREFIX = 'search-result-'; const INDEX_URL = URLS.usoArchiveRaw + 'search-index.json'; + const USW_INDEX_URL = URLS.usw + 'api/index/uso-format'; + const USW_ICON = $create('img', { + src: `${URLS.usw}favicon.ico`, + title: URLS.usw, + }); const STYLUS_CATEGORY = 'chrome-extension'; const PAGE_LENGTH = 10; // update USO style install counter if the style isn't uninstalled immediately @@ -43,8 +48,7 @@ let searchGlobals = $('#search-globals').checked; /** @type string[] */ let query = []; - /** @type 'n' | 'u' | 't' | 'w' | 'r' */ - let order = 't'; + let order = prefs.get('popup.findSort'); let scrollToFirstResult = true; let displayedPage = 1; let totalPages = 1; @@ -98,6 +102,7 @@ $('#search-order').value = order; $('#search-order').onchange = function () { order = this.value; + prefs.set('popup.findSort', order); results.sort(comparator); render(); }; @@ -141,10 +146,9 @@ window.on('styleAdded', async ({detail: {style}}) => { restoreScrollPosition(); - const usoId = calcUsoId(style) || - calcUsoId(await API.styles.get(style.id)); - if (usoId && results.find(r => r.i === usoId)) { - renderActionButtons(usoId, style.id); + const id = calcId(style) || calcId(await API.styles.get(style.id)); + if (id && results.find(r => r.i === id)) { + renderActionButtons(id, style.id); } }); } @@ -181,15 +185,15 @@ show(dom.container); show(dom.list); hide(dom.error); - results = []; try { + results = []; for (let retry = 0; !results.length && retry <= 2; retry++) { results = await search({retry}); } if (results.length) { const installedStyles = await API.styles.getAll(); - const allUsoIds = new Set(installedStyles.map(calcUsoId)); - results = results.filter(r => !allUsoIds.has(r.i)); + const allSupportedIds = new Set(installedStyles.map(calcId)); + results = results.filter(r => !allSupportedIds.has(r.i)); } render(); (results.length ? show : hide)(dom.list); @@ -274,29 +278,39 @@ an: author, sa: shotArchived, sn: shotName, + isUsw, } = entry._result = result; entry.id = RESULT_ID_PREFIX + id; // title Object.assign($('.search-result-title', entry), { onclick: Events.openURLandHide, - href: URLS.usoArchive + `?category=${category}&style=${id}`, + href: isUsw ? `${URLS.usw}style/${id}` : + `${URLS.usoArchive}?category=${category}&style=${id}`, }); + if (isUsw) $('.search-result-title', entry).prepend(USW_ICON.cloneNode(true)); $('.search-result-title span', entry).textContent = t.breakWord(name.length < 300 ? name : name.slice(0, 300) + '...'); // screenshot - const auto = URLS.uso + `auto_style_screenshots/${id}${USO_AUTO_PIC_SUFFIX}`; - Object.assign($('.search-result-screenshot', entry), { - src: shotName && !shotName.endsWith(USO_AUTO_PIC_SUFFIX) - ? `${shotArchived ? URLS.usoArchiveRaw : URLS.uso + 'style_'}screenshots/${shotName}` - : auto, - _src: auto, - onerror: fixScreenshot, - }); + const elShot = $('.search-result-screenshot', entry); + if (isUsw) { + elShot.src = /^https?:/i.test(shotName) ? shotName : BLANK_PIXEL; + } else { + const auto = URLS.uso + `auto_style_screenshots/${id}${USO_AUTO_PIC_SUFFIX}`; + Object.assign(elShot, { + src: shotName && !shotName.endsWith(USO_AUTO_PIC_SUFFIX) + ? `${shotArchived ? URLS.usoArchiveRaw : URLS.uso + 'style_'}screenshots/${shotName}` + : auto, + _src: auto, + onerror: fixScreenshot, + }); + } // author + const eAuthor = encodeURIComponent(author); Object.assign($('[data-type="author"] a', entry), { textContent: author, title: author, - href: URLS.usoArchive + '?author=' + encodeURIComponent(author).replace(/%20/g, '+'), + href: isUsw ? `${URLS.usw}user/${eAuthor}` : + `${URLS.usoArchive}?author=${eAuthor.replace(/%20/g, '+')}`, onclick: Events.openURLandHide, }); // rating @@ -398,18 +412,21 @@ async function install() { const entry = this.closest('.search-result'); const result = /** @type IndexEntry */ entry._result; - const {i: id} = result; + const {i: id, isUsw} = result; const installButton = $('.search-result-install', entry); showSpinner(entry); saveScrollPosition(entry); installButton.disabled = true; entry.style.setProperty('pointer-events', 'none', 'important'); - // 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 (!isUsw) { + // 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`); + } + + const updateUrl = isUsw ? URLS.makeUswCodeUrl(id) : URLS.makeUsoArchiveCodeUrl(id); - const updateUrl = URLS.makeUsoArchiveCodeUrl(id); try { const sourceCode = await download(updateUrl); const style = await API.usercss.install({sourceCode, updateUrl}); @@ -469,8 +486,18 @@ async function fetchIndex() { const timer = setTimeout(showSpinner, BUSY_DELAY, dom.list); - index = (await download(INDEX_URL, {responseType: 'json'})) - .filter(res => res.f === 'uso'); + index = []; + await Promise.all([ + download(INDEX_URL, {responseType: 'json'}).then(res => { + index = index.concat(res.filter(res => res.f === 'uso')); + }).catch(() => {}), + download(USW_INDEX_URL, {responseType: 'json'}).then(res => { + for (const style of res.data) { + style.isUsw = true; + index.push(style); + } + }).catch(() => {}), + ]); clearTimeout(timer); $remove(':scope > .lds-spinner', dom.list); return index; @@ -521,6 +548,14 @@ URLS.extractUsoArchiveId(updateUrl); } + function calcUswId({updateUrl}) { + return URLS.extractUSwId(updateUrl) || 0; + } + + function calcId(style) { + return calcUsoId(style) || calcUswId(style); + } + function calcHaystack(res) { if (!res._nLC) res._nLC = res.n.toLocaleLowerCase(); if (!res._year) res._year = new Date(res.u * 1000).getFullYear();