diff --git a/_locales/en/messages.json b/_locales/en/messages.json index f7cb74fa..27032a06 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1275,6 +1275,10 @@ "message": "Also search global styles", "description": "Checkbox label in the popup's inline style search, shown when the text to search is entered" }, + "searchUserStylesWorld": { + "message": "Search in Userstyles world", + "description": "Checkbox label in the popup's inline style search, shown when the text to search is entered" + }, "searchNumberOfResults": { "message": "Number of matches", "description": "Tooltip for the number of found search results in the editor shown on Ctrl-F" diff --git a/background/style-manager.js b/background/style-manager.js index ed074854..54ce30a0 100644 --- a/background/style-manager.js +++ b/background/style-manager.js @@ -226,6 +226,10 @@ const styleMan = (() => { URLS.extractGreasyForkInstallUrl(style.updateUrl) ); if (url) style.url = style.installationUrl = url; + if (style.initialUrl) { + style.installationUrl = URLS.extractUSwInstallUrl(style.initialUrl); + delete style.initialUrl; + } style.originalDigest = await calcStyleDigest(style); // FIXME: update updateDate? what about usercss config? return handleSave(await saveStyle(style), reason); diff --git a/background/usercss-install-helper.js b/background/usercss-install-helper.js index 6656ac7b..e2cca9a6 100644 --- a/background/usercss-install-helper.js +++ b/background/usercss-install-helper.js @@ -38,6 +38,7 @@ bgReady.all.then(() => { URLS.usoArchiveRaw + 'usercss/*.user.css', '*://greasyfork.org/scripts/*/code/*.user.css', '*://sleazyfork.org/scripts/*/code/*.user.css', + URLS.usw + 'api/style/*.user.css', ...[].concat( ...Object.entries(maybeDistro) .map(([host, {glob}]) => makeUsercssGlobs(host, glob))), diff --git a/install-usercss/preinit.js b/install-usercss/preinit.js index 700c2809..922ee4cb 100644 --- a/install-usercss/preinit.js +++ b/install-usercss/preinit.js @@ -81,6 +81,7 @@ const preinit = (() => { value: API.usercss.buildCode(data.style).then(style => style.sections), configurable: true, }); + data.style.initialUrl = initialUrl; return data; } catch (error) { return {error, sourceCode}; diff --git a/js/prefs.js b/js/prefs.js index 0d1879dc..7b9d3caa 100644 --- a/js/prefs.js +++ b/js/prefs.js @@ -37,6 +37,7 @@ '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 + 'popup.searchUserStylesWorld': false, // use userstyles world instead of uso-archive for inline searching. '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..7d220531 100644 --- a/js/toolbox.js +++ b/js/toolbox.js @@ -78,6 +78,10 @@ const URLS = { usoArchive: 'https://33kk.github.io/uso-archive/', usoArchiveRaw: 'https://raw.githubusercontent.com/33kk/uso-archive/flomaster/data/', + + usw: 'https://userstyles.world/', + uswSearch: 'https://userstyles.world/api/search/', + extractUsoArchiveId: url => url && url.startsWith(URLS.usoArchiveRaw) && @@ -91,6 +95,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}` : ''; + }, + makeUSwArchiveCodeUrl: id => `${URLS.usw}api/style/${id}.user.css`, + supported: url => ( url.startsWith('http') || url.startsWith('ftp') || @@ -411,13 +425,14 @@ function download(url, { }; xhr.onload = () => { if (xhr.status === requiredStatusCode || !requiredStatusCode || u.protocol === 'file:') { + const isUSWSearch = url.startsWith(URLS.uswSearch); const response = expandUsoVars(xhr.response); if (responseHeaders) { const headers = {}; for (const h of responseHeaders) headers[h] = xhr.getResponseHeader(h); resolve({headers, response}); } else { - resolve(response); + resolve(isUSWSearch ? response.data : response); } } else { reject(xhr.status); diff --git a/popup.html b/popup.html index 31f99be9..1cec8c44 100644 --- a/popup.html +++ b/popup.html @@ -252,6 +252,13 @@ +
diff --git a/popup/search.js b/popup/search.js index cac23f0d..0a4b7ad2 100644 --- a/popup/search.js +++ b/popup/search.js @@ -39,8 +39,23 @@ let results; /** @type IndexEntry[] */ let index; + /** + * @typedef USWIndexEntry + * @prop {Number} id - id + * @prop {string} name - name + * @prop {string} username - authorName + * @prop {string} description - description + * @prop {string} preview - screenshot + */ + /** + * @type {Object.} + */ + const USWIndex = {}; + /** @type USWIndexEntry[] */ + let USWResults = []; let category = ''; let searchGlobals = $('#search-globals').checked; + let searchUserStylesWorld = $('#search-usw').checked; /** @type string[] */ let query = []; /** @type 'n' | 'u' | 't' | 'w' | 'r' */ @@ -82,23 +97,36 @@ searchGlobals = this.checked; ready = ready.then(start); }; - $('#search-query').oninput = function () { - query = []; - const text = this.value.trim().toLocaleLowerCase(); - const thisYear = new Date().getFullYear(); - for (let re = /"(.+?)"|(\S+)/g, m; (m = re.exec(text));) { - const n = Number(m[2]); - query.push(n >= 2000 && n <= thisYear ? n : m[1] || m[2]); - } - if (category === STYLUS_CATEGORY && !query.includes('stylus')) { - query.push('stylus'); - } + $('#search-usw').checked = prefs.get('popup.searchUserStylesWorld'); + $('#search-usw').onchange = function () { + searchUserStylesWorld = this.checked; + prefs.set('popup.searchUserStylesWorld', searchUserStylesWorld); ready = ready.then(start); }; + $('#search-query').oninput = function () { + debounce(ev => { + query = []; + const text = ev.value.trim().toLocaleLowerCase(); + const thisYear = new Date().getFullYear(); + for (let re = /"(.+?)"|(\S+)/g, m; (m = re.exec(text));) { + const n = Number(m[2]); + query.push(n >= 2000 && n <= thisYear ? n : m[1] || m[2]); + } + if (category === STYLUS_CATEGORY && !query.includes('stylus')) { + query.push('stylus'); + } + ready = ready.then(start); + }, 750, this); + }; + $('#search-order').value = order; $('#search-order').onchange = function () { order = this.value; - results.sort(comparator); + if (searchUserStylesWorld) { + USWResults.sort(comparator); + } else { + results.sort(comparator); + } render(); }; dom.list = $('#search-results-list'); @@ -132,19 +160,35 @@ window.on('styleDeleted', ({detail: {style: {id}}}) => { restoreScrollPosition(); - const result = results.find(r => r.installedStyleId === id); - if (result) { - clearTimeout(result.pingbackTimer); - renderActionButtons(result.i, -1); + if (searchUserStylesWorld) { + const result = USWResults.find(r => r.id === id); + if (result) { + clearTimeout(result.pingbackTimer); + renderActionButtons(result.id, -1); + } + } else { + const result = results.find(r => r.installedStyleId === id); + if (result) { + clearTimeout(result.pingbackTimer); + renderActionButtons(result.i, -1); + } } }); 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); + if (searchUserStylesWorld) { + const uswId = calcUSwId(style) || + calcUSwId(await API.styles.get(style.id)); + if (uswId && USWResults.find(r => r.id === uswId)) { + renderActionButtons(uswId, style.id); + } + } else { + const usoId = calcUsoId(style) || + calcUsoId(await API.styles.get(style.id)); + if (usoId && results.find(r => r.i === usoId)) { + renderActionButtons(usoId, style.id); + } } }); } @@ -181,19 +225,27 @@ show(dom.container); show(dom.list); hide(dom.error); - results = []; try { - for (let retry = 0; !results.length && retry <= 2; retry++) { - results = await search({retry}); + let resultsMap = []; + for (let retry = 0; !resultsMap.length && retry <= 2; retry++) { + resultsMap = await search({retry, searchUserStylesWorld}); } - if (results.length) { + if (resultsMap.length) { const installedStyles = await API.styles.getAll(); - const allUsoIds = new Set(installedStyles.map(calcUsoId)); - results = results.filter(r => !allUsoIds.has(r.i)); + if (searchUserStylesWorld) { + const allUSwIds = new Set(installedStyles.map(calcUSwId)); + USWResults = resultsMap.filter(r => !allUSwIds.has(r.id)); + } else { + const allUsoIds = new Set(installedStyles.map(calcUsoId)); + results = resultsMap.filter(r => !allUsoIds.has(r.i)); + } + } else { + USWResults = []; + results = []; } render(); - (results.length ? show : hide)(dom.list); - if (!results.length && !$('#search-query').value) { + ((searchUserStylesWorld ? USWResults : results).length ? show : hide)(dom.list); + if (!(searchUserStylesWorld ? USWResults : results).length) { error(t('searchResultNoneFound')); } } catch (reason) { @@ -202,7 +254,8 @@ } function render() { - totalPages = Math.ceil(results.length / PAGE_LENGTH); + const correctResults = searchUserStylesWorld ? USWResults : results; + totalPages = Math.ceil(correctResults.length / PAGE_LENGTH); displayedPage = Math.min(displayedPage, totalPages) || 1; let start = (displayedPage - 1) * PAGE_LENGTH; const end = displayedPage * PAGE_LENGTH; @@ -211,15 +264,17 @@ // keep rendered elements with ids in the range of interest while ( plantAt < PAGE_LENGTH && - slot && slot.id === 'search-result-' + (results[start] || {}).i + slot && slot.id === 'search-result-' + (correctResults[start] || {}).i ) { slot = slot.nextElementSibling; plantAt++; start++; } // add new elements - while (start < Math.min(end, results.length)) { - const entry = createSearchResultNode(results[start++]); + while (start < Math.min(end, correctResults.length)) { + const entry = searchUserStylesWorld + ? createSearchUSWResultNode(correctResults[start++]) + : createSearchResultNode(correctResults[start++]); if (slot) { dom.list.replaceChild(entry, slot); slot = entry.nextElementSibling; @@ -229,13 +284,13 @@ plantAt++; } // remove extraneous elements - const pageLen = end > results.length && - results.length % PAGE_LENGTH || - Math.min(results.length, PAGE_LENGTH); + const pageLen = end > correctResults.length && + correctResults.length % PAGE_LENGTH || + Math.min(correctResults.length, PAGE_LENGTH); while (dom.list.children.length > pageLen) { dom.list.lastElementChild.remove(); } - if (results.length && 'empty' in dom.container.dataset) { + if (correctResults.length && 'empty' in dom.container.dataset) { delete dom.container.dataset.empty; } if (scrollToFirstResult && (!FIREFOX || FIREFOX >= 55)) { @@ -318,6 +373,42 @@ return entry; } + /** + * @param {USWIndexEntry} result + * @returns {Node} + */ + function createSearchUSWResultNode(result) { + const entry = t.template.searchResult.cloneNode(true); + const { + id, + name, + preview, + username, + } = entry._result = result; + entry.id = RESULT_ID_PREFIX + id; + // title + Object.assign($('.search-result-title', entry), { + onclick: Events.openURLandHide, + href: `${URLS.usw}style/${id}`, + }); + $('.search-result-title span', entry).textContent = + t.breakWord(name.length < 300 ? name : name.slice(0, 300) + '...'); + // screenshot + Object.assign($('.search-result-screenshot', entry), { + src: preview, + onerror: fixScreenshot, + }); + // author + Object.assign($('[data-type="author"] a', entry), { + textContent: username, + title: username, + href: `${URLS.usw}user/${encodeURIComponent(username).replace(/%20/g, '+')}`, + onclick: Events.openURLandHide, + }); + renderActionButtons(entry); + return entry; + } + function formatNumber(num) { return ( num > 1e9 ? (num / 1e9).toFixed(1) + 'B' : @@ -369,11 +460,11 @@ } } Object.assign($('.search-result-screenshot', entry), { - onclick: isInstalled ? uninstall : install, + onclick: isInstalled ? uninstall : searchUserStylesWorld ? uswInstall : install, title: isInstalled ? '' : t('installButton'), }); $('.search-result-uninstall', entry).onclick = uninstall; - $('.search-result-install', entry).onclick = install; + $('.search-result-install', entry).onclick = searchUserStylesWorld ? uswInstall : install; } function renderFullInfo(entry, style) { @@ -422,6 +513,30 @@ entry.style.pointerEvents = ''; } + async function uswInstall() { + const entry = this.closest('.search-result'); + const result = /** @type IndexEntry */ entry._result; + const {id} = result; + const installButton = $('.search-result-install', entry); + + showSpinner(entry); + saveScrollPosition(entry); + installButton.disabled = true; + entry.style.setProperty('pointer-events', 'none', 'important'); + + const updateUrl = URLS.makeUSwArchiveCodeUrl(id); + try { + const sourceCode = await download(updateUrl); + const style = await API.usercss.install({sourceCode, updateUrl, initialUrl: updateUrl}); + renderFullInfo(entry, style); + } catch (reason) { + error(`Error while downloading uswID:${id}\nReason: ${reason}`); + } + $remove('.lds-spinner', entry); + installButton.disabled = false; + entry.style.pointerEvents = ''; + } + function uninstall() { const entry = this.closest('.search-result'); saveScrollPosition(entry); @@ -476,10 +591,24 @@ return index; } - async function search({retry} = {}) { + async function searchUSW() { + const timer = setTimeout(showSpinner, BUSY_DELAY, dom.list); + const USWQuery = query.join(' '); + if (!USWQuery) { + return; + } + USWIndex[USWQuery] = (await download(URLS.uswSearch + USWQuery, {responseType: 'json'})); + clearTimeout(timer); + $remove(':scope > .lds-spinner', dom.list); + return USWIndex[USWQuery]; + } + + async function search({retry, searchUserStylesWorld} = {}) { return retry && !calcCategory({retry}) - ? [] - : (index || await fetchIndex()).filter(isResultMatching).sort(comparator); + ? [] + : searchUserStylesWorld + ? (USWIndex[query.join(' ')] || await searchUSW() || []).sort(comparator) + : (index || await fetchIndex()).filter(isResultMatching).sort(comparator); } function isResultMatching(res) { @@ -521,6 +650,10 @@ URLS.extractUsoArchiveId(updateUrl); } + function calcUSwId({installationUrl}) { + return installationUrl ? installationUrl.match(/\d+|$/)[0] : 0; + } + function calcHaystack(res) { if (!res._nLC) res._nLC = res.n.toLocaleLowerCase(); if (!res._year) res._year = new Date(res.u * 1000).getFullYear();