@@ -254,6 +252,20 @@
diff --git a/popup/hotkeys.js b/popup/hotkeys.js
index fe058ea1..157a34bb 100644
--- a/popup/hotkeys.js
+++ b/popup/hotkeys.js
@@ -33,7 +33,8 @@ const hotkeys = (() => {
}
function onKeyDown(event) {
- if (event.ctrlKey || event.altKey || event.metaKey || !enabled) {
+ if (event.ctrlKey || event.altKey || event.metaKey || !enabled ||
+ /^(text|search)$/.test((document.activeElement || {}).type)) {
return;
}
let entry;
diff --git a/popup/popup.js b/popup/popup.js
index f6aea8c9..fb2b10bf 100644
--- a/popup/popup.js
+++ b/popup/popup.js
@@ -13,7 +13,8 @@ const handleEvent = {};
const ABOUT_BLANK = 'about:blank';
const ENTRY_ID_PREFIX_RAW = 'style-';
-const ENTRY_ID_PREFIX = '#' + ENTRY_ID_PREFIX_RAW;
+
+$.entry = styleOrId => $(`#${ENTRY_ID_PREFIX_RAW}${styleOrId.id || styleOrId}`);
if (CHROME >= 66 && CHROME <= 69) { // Chrome 66-69 adds a gap, https://crbug.com/821143
document.head.appendChild($create('style', 'html { overflow: overlay }'));
@@ -27,7 +28,7 @@ initTabUrls()
onDOMready().then(() => initPopup(frames)),
...frames
.filter(f => f.url && !f.isDupe)
- .map(({url}) => API.getStylesByUrl(url).then(styles => ({styles, url}))),
+ .map(({url}) => getStyleDataMerged(url).then(styles => ({styles, url}))),
]))
.then(([, ...results]) => {
if (results[0]) {
@@ -53,17 +54,19 @@ if (CHROME_HAS_BORDER_BUG) {
}
function onRuntimeMessage(msg) {
+ if (!tabURL) return;
+ let ready = Promise.resolve();
switch (msg.method) {
case 'styleAdded':
case 'styleUpdated':
if (msg.reason === 'editPreview' || msg.reason === 'editPreviewEnd') return;
- handleUpdate(msg);
+ ready = handleUpdate(msg);
break;
case 'styleDeleted':
handleDelete(msg.style.id);
break;
}
- dispatchEvent(new CustomEvent(msg.method, {detail: msg}));
+ ready.then(() => dispatchEvent(new CustomEvent(msg.method, {detail: msg})));
}
@@ -141,8 +144,7 @@ function initPopup(frames) {
}
if (!tabURL) {
- document.body.classList.add('blocked');
- document.body.insertBefore(template.unavailableInfo, document.body.firstChild);
+ blockPopup();
return;
}
@@ -315,24 +317,30 @@ function showStyles(frameResults) {
const entries = new Map();
frameResults.forEach(({styles = [], url}, index) => {
styles.forEach(style => {
- const {id} = style.data;
+ const {id} = style;
if (!entries.has(id)) {
style.frameUrl = index === 0 ? '' : url;
- entries.set(id, createStyleElement(Object.assign(style.data, style)));
+ entries.set(id, createStyleElement(style));
}
});
});
if (entries.size) {
- installed.append(...sortStyles([...entries.values()]));
+ resortEntries([...entries.values()]);
} else {
- installed.appendChild(template.noStyles.cloneNode(true));
+ installed.appendChild(template.noStyles);
}
window.dispatchEvent(new Event('showStyles:done'));
}
+function resortEntries(entries) {
+ // `entries` is specified only at startup, after that we respect the prefs
+ if (entries || prefs.get('popup.autoResort')) {
+ installed.append(...sortStyles(entries || $$('.entry', installed)));
+ }
+}
function createStyleElement(style) {
- let entry = $(ENTRY_ID_PREFIX + style.id);
+ let entry = $.entry(style);
if (!entry) {
entry = template.style.cloneNode(true);
entry.setAttribute('style-id', style.id);
@@ -469,11 +477,7 @@ Object.assign(handleEvent, {
event.stopPropagation();
API
.toggleStyle(handleEvent.getClickedStyleId(event), this.checked)
- .then(() => {
- if (prefs.get('popup.autoResort')) {
- installed.append(...sortStyles($$('.entry', installed)));
- }
- });
+ .then(() => resortEntries());
},
toggleExclude(event, type) {
@@ -672,38 +676,25 @@ Object.assign(handleEvent, {
});
-function handleUpdate({style, reason}) {
- if (!tabURL) return;
-
- fetchStyle()
- .then(style => {
- if (!style) {
- return;
- }
- if ($(ENTRY_ID_PREFIX + style.id)) {
- createStyleElement(style);
- return;
- }
- document.body.classList.remove('blocked');
- $$.remove('.blocked-info, #no-styles');
- createStyleElement(style);
- })
- .catch(console.error);
-
- function fetchStyle() {
- if (reason === 'toggle' && $(ENTRY_ID_PREFIX + style.id)) {
- return Promise.resolve(style);
- }
- return API.getStylesByUrl(tabURL, style.id)
- .then(([result]) => result && Object.assign(result.data, result));
+async function handleUpdate({style, reason}) {
+ if (reason !== 'toggle' || !$.entry(style)) {
+ style = await getStyleDataMerged(tabURL, style.id);
+ if (!style) return;
}
+ const el = createStyleElement(style);
+ if (!el.parentNode) {
+ installed.appendChild(el);
+ blockPopup(false);
+ }
+ resortEntries();
}
function handleDelete(id) {
- $.remove(ENTRY_ID_PREFIX + id);
- if (!$('.entry')) {
- installed.appendChild(template.noStyles.cloneNode(true));
+ const el = $.entry(id);
+ if (el) {
+ el.remove();
+ if (!$('.entry')) installed.appendChild(template.noStyles);
}
}
@@ -721,3 +712,21 @@ function waitForTabUrlFF(tab) {
]);
});
}
+
+/* Merges the extra props from API into style data.
+ * When `id` is specified returns a single object otherwise an array */
+async function getStyleDataMerged(url, id) {
+ const styles = (await API.getStylesByUrl(url, id))
+ .map(r => Object.assign(r.data, r));
+ return id ? styles[0] : styles;
+}
+
+function blockPopup(isBlocked = true) {
+ document.body.classList.toggle('blocked', isBlocked);
+ if (isBlocked) {
+ document.body.prepend(template.unavailableInfo);
+ } else {
+ template.unavailableInfo.remove();
+ template.noStyles.remove();
+ }
+}
diff --git a/popup/search-results.css b/popup/search-results.css
index e1c805a6..d4c8d4e9 100755
--- a/popup/search-results.css
+++ b/popup/search-results.css
@@ -54,21 +54,15 @@ body.search-results-shown {
background-color: #fff;
}
-.search-result .lds-spinner {
+#search-results .lds-spinner {
transform: scale(.5);
filter: invert(1) drop-shadow(1px 1px 3px #000);
}
-.search-result-empty .lds-spinner {
- transform: scale(.5);
+#search-results .search-result-empty .lds-spinner {
filter: opacity(.2);
}
-.search-result-fadein {
- animation: fadein 1s;
- animation-fill-mode: both;
-}
-
.search-result-screenshot {
height: 140px;
width: 100%;
@@ -257,6 +251,18 @@ body.search-results-shown {
padding-left: 16px;
}
+#search-params {
+ display: flex;
+ position: relative;
+ margin-bottom: 1.25rem;
+}
+
+#search-query {
+ min-width: 3em;
+ margin-right: .5em;
+ flex: auto;
+}
+
/* spinner: https://github.com/loadingio/css-spinner */
.lds-spinner {
-webkit-user-select: none;
diff --git a/popup/search-results.js b/popup/search-results.js
index d46982ac..a29b6911 100755
--- a/popup/search-results.js
+++ b/popup/search-results.js
@@ -1,105 +1,95 @@
-/* global tabURL handleEvent $ $$ prefs template FIREFOX chromeLocal debounce
- $create t API tWordBreak formatDate tryCatch tryJSONparse LZString
- promisifyChrome download */
+/* global URLS tabURL handleEvent $ $$ prefs template FIREFOX debounce
+ $create t API tWordBreak formatDate tryCatch download */
'use strict';
-window.addEventListener('showStyles:done', function _() {
- window.removeEventListener('showStyles:done', _);
-
- if (!tabURL) {
- return;
- }
-
- //region Init
-
- const BODY_CLASS = 'search-results-shown';
+window.addEventListener('showStyles:done', () => {
+ if (!tabURL) return;
const RESULT_ID_PREFIX = 'search-result-';
-
- const BASE_URL = 'https://userstyles.org';
- const JSON_URL = BASE_URL + '/styles/chrome/';
- const API_URL = BASE_URL + '/api/v1/styles/';
- const UPDATE_URL = 'https://update.userstyles.org/%.md5';
-
+ const INDEX_URL = URLS.usoArchiveRaw + 'search-index.json';
const STYLUS_CATEGORY = 'chrome-extension';
-
- const DISPLAY_PER_PAGE = 10;
- // Millisecs to wait before fetching next batch of search results.
- const DELAY_AFTER_FETCHING_STYLES = 0;
- // Millisecs to wait before fetching .JSON for next search result.
- const DELAY_BEFORE_SEARCHING_STYLES = 0;
-
- // update USO style install counter
- // if the style isn't uninstalled in the popup
- const PINGBACK_DELAY = 60e3;
-
- const BLANK_PIXEL_DATA = 'data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAA' +
- 'C1HAwCAAAAC0lEQVR42mOcXQ8AAbsBHLLDr5MAAAAASUVORK5CYII=';
-
- const CACHE_SIZE = 1e6;
- const CACHE_PREFIX = 'usoSearchCache/';
- const CACHE_DURATION = 24 * 3600e3;
- const CACHE_CLEANUP_THROTTLE = 10e3;
- const CACHE_CLEANUP_NEEDED = CACHE_PREFIX + 'clean?';
- const CACHE_EXCEPT_PROPS = ['css', 'discussions', 'additional_info'];
-
- let searchTotalPages;
- let searchCurrentPage = 1;
- let searchExhausted = 0; // 1: once, 2: twice (first host.jp, then host)
-
- // currently active USO requests
- const xhrSpoofIds = new Set();
- // used as an HTTP header name to identify spoofed requests
- const xhrSpoofTelltale = getRandomId();
-
- const processedResults = [];
- const unprocessedResults = [];
-
- let loading = false;
- // Category for the active tab's URL.
- let category;
+ const PAGE_LENGTH = 10;
+ // update USO style install counter if the style isn't uninstalled immediately
+ const PINGBACK_DELAY = 5e3;
+ const BUSY_DELAY = .5e3;
+ const USO_AUTO_PIC_SUFFIX = '-after.png';
+ const BLANK_PIXEL = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
+ const dom = {};
+ /**
+ * @typedef IndexEntry
+ * @prop {'uso' | 'uso-android'} f - format
+ * @prop {Number} i - id
+ * @prop {string} n - name
+ * @prop {string} c - category
+ * @prop {Number} u - updatedTime
+ * @prop {Number} t - totalInstalls
+ * @prop {Number} w - weeklyInstalls
+ * @prop {Number} r - rating
+ * @prop {Number} ai - authorId
+ * @prop {string} an - authorName
+ * @prop {string} sn - screenshotName
+ * @prop {boolean} sa - screenshotArchived
+ */
+ /** @type IndexEntry[] */
+ let results;
+ /** @type IndexEntry[] */
+ let index;
+ let category = '';
+ /** @type string[] */
+ let query = [];
+ /** @type 'n' | 'u' | 't' | 'w' | 'r' */
+ let order = 't';
let scrollToFirstResult = true;
-
let displayedPage = 1;
let totalPages = 1;
- let totalResults = 0;
+ let ready;
- // fade-in when the entry took that long to replace its placeholder
- const FADEIN_THRESHOLD = 50;
+ calcCategory();
- const dom = {};
+ const $orNode = (sel, base) => sel instanceof Node ? sel : $(sel, base);
+ const show = (...args) => $orNode(...args).classList.remove('hidden');
+ const hide = (...args) => $orNode(...args).classList.add('hidden');
Object.assign($('#find-styles-link'), {
- href: BASE_URL + '/styles/browse/' + getCategory(),
+ href: URLS.usoArchive,
onclick(event) {
if (!prefs.get('popup.findStylesInline') || dom.container) {
+ this.search = `${new URLSearchParams({category, search: $('#search-query').value})}`;
handleEvent.openURLandHide.call(this, event);
return;
}
event.preventDefault();
-
this.textContent = this.title;
this.title = '';
-
init();
- load();
+ ready = start();
},
});
return;
function init() {
- promisifyChrome({
- 'storage.local': ['getBytesInUse'], // FF doesn't implement it
- });
- setTimeout(() => document.body.classList.add(BODY_CLASS));
-
- $('#find-styles-inline-group').classList.add('hidden');
-
+ setTimeout(() => document.body.classList.add('search-results-shown'));
+ hide('#find-styles-inline-group');
+ $('#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]);
+ }
+ ready = ready.then(start);
+ };
+ $('#search-order').value = order;
+ $('#search-order').onchange = function () {
+ order = this.value;
+ results.sort(comparator);
+ render();
+ };
+ dom.list = $('#search-results-list');
dom.container = $('#search-results');
dom.container.dataset.empty = '';
-
dom.error = $('#search-results-error');
-
dom.nav = {};
const navOnClick = {prev, next};
for (const place of ['top', 'bottom']) {
@@ -113,10 +103,6 @@ window.addEventListener('showStyles:done', function _() {
}
}
- dom.list = $('#search-results-list');
-
- addEventListener('scroll', loadMoreIfNeeded, {passive: true});
-
if (FIREFOX) {
let lastShift;
addEventListener('resize', () => {
@@ -130,43 +116,24 @@ window.addEventListener('showStyles:done', function _() {
}
addEventListener('styleDeleted', ({detail: {style: {id}}}) => {
- const result = processedResults.find(r => r.installedStyleId === id);
+ restoreScrollPosition();
+ const result = results.find(r => r.installedStyleId === id);
if (result) {
- result.installed = false;
- result.installedStyleId = -1;
- window.clearTimeout(result.pingbackTimer);
- renderActionButtons($('#' + RESULT_ID_PREFIX + result.id));
+ clearTimeout(result.pingbackTimer);
+ renderActionButtons(result.i, -1);
}
});
- addEventListener('styleAdded', ({detail: {style: {id, md5Url}}}) => {
- const usoId = parseInt(md5Url && md5Url.match(/\d+|$/)[0]);
- const result = usoId && processedResults.find(r => r.id === usoId);
- if (result) {
- result.installed = true;
- result.installedStyleId = id;
- renderActionButtons($('#' + RESULT_ID_PREFIX + usoId));
+ addEventListener('styleAdded', async ({detail: {style}}) => {
+ restoreScrollPosition();
+ const usoId = calcUsoId(style) ||
+ calcUsoId(await API.getStyle(style.id, true));
+ if (usoId && results.find(r => r.i === usoId)) {
+ renderActionButtons(usoId, style.id);
}
});
-
- chromeLocal.getValue(CACHE_CLEANUP_NEEDED).then(value =>
- value && debounce(cleanupCache, CACHE_CLEANUP_THROTTLE));
}
- //endregion
- //region Loader
-
- /**
- * Sets loading status of search results.
- * @param {Boolean} isLoading If search results are idle (false) or still loading (true).
- */
- function setLoading(isLoading) {
- if (loading !== isLoading) {
- loading = isLoading;
- // Refresh elements that depend on `loading` state.
- render();
- }
- }
function showSpinner(parent) {
parent = parent instanceof Node ? parent : $(parent);
@@ -174,166 +141,92 @@ window.addEventListener('showStyles:done', function _() {
new Array(12).fill($create('div')).map(e => e.cloneNode())));
}
- /** Increments displayedPage and loads results. */
function next() {
- if (loading) {
- debounce(next, 100);
- return;
- }
- displayedPage += 1;
+ displayedPage = Math.min(totalPages, displayedPage + 1);
scrollToFirstResult = true;
render();
- loadMoreIfNeeded();
}
- /** Decrements currentPage and loads results. */
function prev() {
- if (loading) {
- debounce(next, 100);
- return;
- }
displayedPage = Math.max(1, displayedPage - 1);
scrollToFirstResult = true;
render();
}
- /**
- * Display error message to user.
- * @param {string} message Message to display to user.
- */
function error(reason) {
- dom.error.textContent = reason === 404 ? t('searchResultNoneFound') : reason;
- dom.error.classList.remove('hidden');
- dom.container.classList.toggle('hidden', !processedResults.length);
- document.body.classList.toggle('search-results-shown', processedResults.length > 0);
+ dom.error.textContent = reason;
+ show(dom.error);
+ (results.length ? show : hide)(dom.container);
+ document.body.classList.toggle('search-results-shown', results.length > 0);
if (dom.error.getBoundingClientRect().bottom < 0) {
dom.error.scrollIntoView({behavior: 'smooth', block: 'start'});
}
}
- /**
- * Initializes search results container, starts fetching results.
- */
- function load() {
- if (searchExhausted > 1) {
- if (!processedResults.length) {
- error(404);
+ async function start() {
+ show(dom.container);
+ hide(dom.error);
+ results = [];
+ try {
+ for (let retry = 0; !results.length && retry <= 2; retry++) {
+ results = await search({retry});
}
- return;
- }
-
- setLoading(true);
- dom.container.classList.remove('hidden');
- dom.error.classList.add('hidden');
-
- category = category || getCategory();
-
- search({category})
- .then(function process(results) {
- const data = results.data.filter(sameCategoryNoDupes);
-
- if (!data.length && searchExhausted <= 1) {
- const old = category;
- const uso = (processedResults[0] || {}).subcategory;
- category = uso !== category && uso || getCategory({retry: true});
- if (category !== old) return search({category, restart: true}).then(process);
- }
-
- const numIrrelevant = results.data.length - data.length;
- totalResults += results.current_page === 1 ? results.total_entries : 0;
- totalResults = Math.max(0, totalResults - numIrrelevant);
- totalPages = Math.ceil(totalResults / DISPLAY_PER_PAGE);
-
- setLoading(false);
-
- if (data.length) {
- unprocessedResults.push(...data);
- processNextResult();
- } else if (numIrrelevant) {
- load();
- } else if (!processedResults.length) {
- return Promise.reject(404);
- }
- })
- .catch(error);
- }
-
- function loadMoreIfNeeded(event) {
- let pageToPrefetch = displayedPage;
- if (event instanceof Event) {
- if ((loadMoreIfNeeded.prefetchedPage || 0) <= pageToPrefetch &&
- document.scrollingElement.scrollTop > document.scrollingElement.scrollHeight / 2) {
- loadMoreIfNeeded.prefetchedPage = ++pageToPrefetch;
- } else {
- return;
+ if (results.length) {
+ const installedStyles = await API.getAllStyles(true);
+ const allUsoIds = new Set(installedStyles.map(calcUsoId));
+ results = results.filter(r => !allUsoIds.has(r.i));
}
- }
- if (processedResults.length < pageToPrefetch * DISPLAY_PER_PAGE) {
- setTimeout(load, DELAY_BEFORE_SEARCHING_STYLES);
- }
- }
-
- /**
- * Processes the next search result in `unprocessedResults` and adds to `processedResults`.
- * Skips installed/non-applicable styles.
- * Fetches more search results if unprocessedResults is empty.
- * Recurses until shouldLoadMore() is false.
- */
- function processNextResult() {
- const result = unprocessedResults.shift();
- if (!result) {
- loadMoreIfNeeded();
- return;
- }
- const md5Url = UPDATE_URL.replace('%', result.id);
- API.styleExists({md5Url}).then(exist => {
- if (exist) {
- totalResults = Math.max(0, totalResults - 1);
- } else {
- processedResults.push(result);
+ if (results.length || $('#search-query').value) {
render();
+ } else {
+ error(t('searchResultNoneFound'));
}
- setTimeout(processNextResult, !exist && DELAY_AFTER_FETCHING_STYLES);
- });
+ } catch (reason) {
+ error(reason);
+ }
}
- //endregion
- //region UI
-
function render() {
- let start = (displayedPage - 1) * DISPLAY_PER_PAGE;
- const end = displayedPage * DISPLAY_PER_PAGE;
-
+ totalPages = Math.ceil(results.length / PAGE_LENGTH);
+ displayedPage = Math.min(displayedPage, totalPages) || 1;
+ let start = (displayedPage - 1) * PAGE_LENGTH;
+ const end = displayedPage * PAGE_LENGTH;
let plantAt = 0;
let slot = dom.list.children[0];
-
// keep rendered elements with ids in the range of interest
while (
- plantAt < DISPLAY_PER_PAGE &&
- slot && slot.id === 'search-result-' + (processedResults[start] || {}).id
+ plantAt < PAGE_LENGTH &&
+ slot && slot.id === 'search-result-' + (results[start] || {}).i
) {
slot = slot.nextElementSibling;
plantAt++;
start++;
}
-
- const plantEntry = entry => {
+ // add new elements
+ while (start < Math.min(end, results.length)) {
+ const entry = createSearchResultNode(results[start++]);
if (slot) {
dom.list.replaceChild(entry, slot);
slot = entry.nextElementSibling;
} else {
dom.list.appendChild(entry);
}
- entry.classList.toggle('search-result-fadein',
- !slot || performance.now() - slot._plantedTime > FADEIN_THRESHOLD);
- return entry;
- };
-
- while (start < Math.min(end, processedResults.length)) {
- plantEntry(createSearchResultNode(processedResults[start++]));
plantAt++;
}
-
+ // remove extraneous elements
+ const pageLen = end > results.length &&
+ results.length % PAGE_LENGTH ||
+ Math.min(results.length, PAGE_LENGTH);
+ while (dom.list.children.length > pageLen) {
+ dom.list.lastElementChild.remove();
+ }
+ if (results.length && 'empty' in dom.container.dataset) {
+ delete dom.container.dataset.empty;
+ }
+ if (scrollToFirstResult && (!FIREFOX || FIREFOX >= 55)) {
+ debounce(doScrollToFirstResult);
+ }
+ // navigation
for (const place in dom.nav) {
const nav = dom.nav[place];
nav._prev.disabled = displayedPage <= 1;
@@ -341,34 +234,6 @@ window.addEventListener('showStyles:done', function _() {
nav._page.textContent = displayedPage;
nav._total.textContent = totalPages;
}
-
- // Fill in remaining search results with blank results + spinners
- const maxResults = end > totalResults &&
- totalResults % DISPLAY_PER_PAGE ||
- DISPLAY_PER_PAGE;
- while (plantAt < maxResults) {
- if (!slot || slot.id.startsWith(RESULT_ID_PREFIX)) {
- const entry = plantEntry(template.emptySearchResult.cloneNode(true));
- entry._plantedTime = performance.now();
- showSpinner(entry);
- }
- plantAt++;
- if (!processedResults.length) {
- break;
- }
- }
-
- while (dom.list.children.length > maxResults) {
- dom.list.lastElementChild.remove();
- }
-
- if (processedResults.length && 'empty' in dom.container.dataset) {
- delete dom.container.dataset.empty;
- }
-
- if (scrollToFirstResult && (!FIREFOX || FIREFOX >= 55)) {
- debounce(doScrollToFirstResult);
- }
}
function doScrollToFirstResult() {
@@ -379,96 +244,61 @@ window.addEventListener('showStyles:done', function _() {
}
/**
- * Constructs and adds the given search result to the popup's Search Results container.
- * @param {Object} result The SearchResult object from userstyles.org
+ * @param {IndexEntry} result
+ * @returns {Node}
*/
function createSearchResultNode(result) {
- /*
- userstyleSearchResult format: {
- id: 100835,
- name: "Reddit Flat Dark",
- screenshot_url: "19339_after.png",
- description: "...",
- user: {
- id: 48470,
- name: "holloh"
- },
- style_settings: [...]
- }
- */
-
const entry = template.searchResult.cloneNode(true);
- Object.assign(entry, {
- _result: result,
- id: RESULT_ID_PREFIX + result.id,
- });
-
+ const {
+ i: id,
+ n: name,
+ r: rating,
+ u: updateTime,
+ w: weeklyInstalls,
+ t: totalInstalls,
+ an: author,
+ sa: shotArchived,
+ sn: shotName,
+ } = entry._result = result;
+ entry.id = RESULT_ID_PREFIX + id;
+ // title
Object.assign($('.search-result-title', entry), {
onclick: handleEvent.openURLandHide,
- href: BASE_URL + result.url
+ href: URLS.usoArchive + `?category=${category}&style=${id}`
});
-
- const displayedName = result.name.length < 300 ? result.name : result.name.slice(0, 300) + '...';
- $('.search-result-title span', entry).textContent = tWordBreak(displayedName);
-
- const screenshot = $('.search-result-screenshot', entry);
- let url = result.screenshot_url;
- if (!url) {
- url = BLANK_PIXEL_DATA;
- screenshot.classList.add('no-screenshot');
- } else if (/^[0-9]*_after.(jpe?g|png|gif)$/i.test(url)) {
- url = BASE_URL + '/style_screenshot_thumbnails/' + url;
- }
- screenshot.src = url;
- if (url !== BLANK_PIXEL_DATA) {
- screenshot.classList.add('search-result-fadein');
- screenshot.onload = () => {
- screenshot.classList.remove('search-result-fadein');
- };
- }
-
- const description = result.description
- .replace(/<[^>]*>/g, ' ')
- .replace(/([^.][.。?!]|[\s,].{50,70})\s+/g, '$1\n')
- .replace(/([\r\n]\s*){3,}/g, '\n\n');
- Object.assign($('.search-result-description', entry), {
- textContent: description,
- title: description,
+ $('.search-result-title span', entry).textContent =
+ tWordBreak(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,
});
-
+ // author
Object.assign($('[data-type="author"] a', entry), {
- textContent: result.user.name,
- title: result.user.name,
- href: BASE_URL + '/users/' + result.user.id,
+ textContent: author,
+ title: author,
+ href: URLS.usoArchive + '?author=' + encodeURIComponent(author).replace(/%20/g, '+'),
onclick: handleEvent.openURLandHide,
});
-
- let ratingClass;
- let ratingValue = result.rating;
- if (ratingValue === null) {
- ratingClass = 'none';
- ratingValue = '';
- } else if (ratingValue >= 2.5) {
- ratingClass = 'good';
- ratingValue = ratingValue.toFixed(1);
- } else if (ratingValue >= 1.5) {
- ratingClass = 'okay';
- ratingValue = ratingValue.toFixed(1);
- } else {
- ratingClass = 'bad';
- ratingValue = ratingValue.toFixed(1);
- }
- $('[data-type="rating"]', entry).dataset.class = ratingClass;
- $('[data-type="rating"] dd', entry).textContent = ratingValue;
-
+ // rating
+ $('[data-type="rating"]', entry).dataset.class =
+ !rating ? 'none' :
+ rating >= 2.5 ? 'good' :
+ rating >= 1.5 ? 'okay' :
+ 'bad';
+ $('[data-type="rating"] dd', entry).textContent = rating && rating.toFixed(1) || '';
+ // time
Object.assign($('[data-type="updated"] time', entry), {
- dateTime: result.updated,
- textContent: formatDate(result.updated)
+ dateTime: updateTime * 1000,
+ textContent: formatDate(updateTime * 1000)
});
-
- $('[data-type="weekly"] dd', entry).textContent = formatNumber(result.weekly_install_count);
- $('[data-type="total"] dd', entry).textContent = formatNumber(result.total_install_count);
-
+ // totals
+ $('[data-type="weekly"] dd', entry).textContent = formatNumber(weeklyInstalls);
+ $('[data-type="total"] dd', entry).textContent = formatNumber(totalInstalls);
renderActionButtons(entry);
return entry;
}
@@ -484,141 +314,123 @@ window.addEventListener('showStyles:done', function _() {
);
}
- function renderActionButtons(entry) {
- if (!entry) {
- return;
+ function fixScreenshot() {
+ const {_src} = this;
+ if (_src && _src !== this.src) {
+ this.src = _src;
+ delete this._src;
+ } else {
+ this.src = BLANK_PIXEL;
+ this.onerror = null;
}
- const result = entry._result;
+ }
- if (result.installed && !('installed' in entry.dataset)) {
+ function renderActionButtons(entry, installedId) {
+ if (Number(entry)) {
+ entry = $('#' + RESULT_ID_PREFIX + entry);
+ }
+ if (!entry) return;
+ const result = entry._result;
+ if (typeof installedId === 'number') {
+ result.installed = installedId > 0;
+ result.installedStyleId = installedId;
+ }
+ const isInstalled = result.installed;
+ if (isInstalled && !('installed' in entry.dataset)) {
entry.dataset.installed = '';
$('.search-result-status', entry).textContent = t('clickToUninstall');
- } else if (!result.installed && 'installed' in entry.dataset) {
+ } else if (!isInstalled && 'installed' in entry.dataset) {
delete entry.dataset.installed;
$('.search-result-status', entry).textContent = '';
+ hide('.search-result-customize', entry);
}
+ Object.assign($('.search-result-screenshot', entry), {
+ onclick: isInstalled ? uninstall : install,
+ title: isInstalled ? '' : t('installButton'),
+ });
+ $('.search-result-uninstall', entry).onclick = uninstall;
+ $('.search-result-install', entry).onclick = install;
+ }
- const screenshot = $('.search-result-screenshot', entry);
- screenshot.onclick = result.installed ? onUninstallClicked : onInstallClicked;
- screenshot.title = result.installed ? '' : t('installButton');
-
- const uninstallButton = $('.search-result-uninstall', entry);
- uninstallButton.onclick = onUninstallClicked;
-
- const installButton = $('.search-result-install', entry);
- installButton.onclick = onInstallClicked;
- if ((result.style_settings || []).length > 0) {
- // Style has customizations
- installButton.classList.add('customize');
- uninstallButton.classList.add('customize');
-
- const customizeButton = $('.search-result-customize', entry);
- customizeButton.dataset.href = BASE_URL + result.url;
- customizeButton.dataset.sendMessage = JSON.stringify({method: 'openSettings'});
- customizeButton.classList.remove('hidden');
- customizeButton.onclick = function (event) {
- event.stopPropagation();
- handleEvent.openURLandHide.call(this, event);
- };
+ function renderFullInfo(entry, style) {
+ let {description, vars} = style.usercssData;
+ // description
+ description = (description || '')
+ .replace(/<[^>]*>/g, ' ')
+ .replace(/([^.][.。?!]|[\s,].{50,70})\s+/g, '$1\n')
+ .replace(/([\r\n]\s*){3,}/g, '\n\n');
+ Object.assign($('.search-result-description', entry), {
+ textContent: description,
+ title: description,
+ });
+ // config button
+ if (vars) {
+ const btn = $('.search-result-customize', entry);
+ btn.onclick = () => $('.configure', $.entry(style)).click();
+ show(btn);
}
}
- function onUninstallClicked(event) {
- event.stopPropagation();
+ async function install() {
const entry = this.closest('.search-result');
- saveScrollPosition(entry);
- API.deleteStyle(entry._result.installedStyleId)
- .then(restoreScrollPosition);
- }
-
- /** Installs the current userstyleSearchResult into Stylus. */
- function onInstallClicked(event) {
- event.stopPropagation();
-
- const entry = this.closest('.search-result');
- const result = entry._result;
+ const result = /** @type IndexEntry */ entry._result;
+ const {i: id} = 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`);
- // Fetch settings to see if we should display "configure" button
- Promise.all([
- fetchStyleJson(result),
- fetchStyleSettings(result),
- API.download({url: UPDATE_URL.replace('%', result.id)})
- ])
- .then(([style, settings, md5]) => {
- pingback(result);
- // show a 'config-on-homepage' icon in the popup
- style.updateUrl += settings.length ? '?' : '';
- style.originalMd5 = md5;
- return API.installStyle(style);
- })
- .catch(reason => {
- const usoId = result.id;
- console.debug('install:saveStyle(usoID:', usoId, ') => [ERROR]: ', reason);
- error('Error while downloading usoID:' + usoId + '\nReason: ' + reason);
- })
- .then(() => {
- $.remove('.lds-spinner', entry);
- installButton.disabled = false;
- entry.style.pointerEvents = '';
- restoreScrollPosition();
- });
-
- function fetchStyleSettings(result) {
- return result.style_settings ||
- fetchStyle(result.id).then(style => {
- result.style_settings = style.style_settings || [];
- return result.style_settings;
- });
+ const updateUrl = `${URLS.usoArchiveRaw}usercss/${id}.user.css`;
+ try {
+ const sourceCode = await download(updateUrl);
+ const style = await API.installUsercss({sourceCode, updateUrl});
+ renderFullInfo(entry, style);
+ } catch (reason) {
+ error(`Error while downloading usoID:${id}\nReason: ${reason}`);
}
+ $.remove('.lds-spinner', entry);
+ installButton.disabled = false;
+ entry.style.pointerEvents = '';
}
- function pingback(result) {
- const wnd = window;
- // FIXME: move this to background page and create an API like installUSOStyle
- result.pingbackTimer = wnd.setTimeout(wnd.download, PINGBACK_DELAY,
- BASE_URL + '/styles/install/' + result.id + '?source=stylish-ch');
+ function uninstall() {
+ const entry = this.closest('.search-result');
+ saveScrollPosition(entry);
+ API.deleteStyle(entry._result.installedStyleId);
}
function saveScrollPosition(entry) {
- dom.scrollPosition = entry.getBoundingClientRect().top;
- dom.scrollPositionElement = entry;
+ dom.scrollPos = entry.getBoundingClientRect().top;
+ dom.scrollPosElement = entry;
}
function restoreScrollPosition() {
- const t0 = performance.now();
- new MutationObserver((mutations, observer) => {
- if (performance.now() - t0 < 1000) {
- window.scrollBy(0, dom.scrollPositionElement.getBoundingClientRect().top - dom.scrollPosition);
- }
- observer.disconnect();
- }).observe(document.body, {childList: true, subtree: true, attributes: true});
+ window.scrollBy(0, dom.scrollPosElement.getBoundingClientRect().top - dom.scrollPos);
}
- //endregion
- //region USO API wrapper
-
/**
* Resolves the Userstyles.org "category" for a given URL.
+ * @returns {boolean} true if the category has actually changed
*/
- function getCategory({retry} = {}) {
+ function calcCategory({retry} = {}) {
const u = tryCatch(() => new URL(tabURL));
+ const old = category;
if (!u) {
// Invalid URL
- return '';
+ category = '';
} else if (u.protocol === 'file:') {
- return 'file:';
+ category = 'file:';
} else if (u.protocol === location.protocol) {
- return STYLUS_CATEGORY;
+ category = STYLUS_CATEGORY;
} else {
const parts = u.hostname.replace(/\.(?:com?|org)(\.\w{2,3})$/, '$1').split('.');
const [tld, main = u.hostname, third, fourth] = parts.reverse();
- const keepTld = !retry && !(
+ const keepTld = retry !== 1 && !(
tld === 'com' ||
tld === 'org' && main !== 'userstyles'
);
@@ -626,214 +438,61 @@ window.addEventListener('showStyles:done', function _() {
fourth ||
third && third !== 'www' && third !== 'm'
);
- return (keepThird && `${third}.` || '') + main + (keepTld || keepThird ? `.${tld}` : '');
+ category = (keepThird && `${third}.` || '') + main + (keepTld || keepThird ? `.${tld}` : '');
}
+ return category !== old;
}
- function sameCategoryNoDupes(result) {
+ async function fetchIndex() {
+ const timer = setTimeout(showSpinner, BUSY_DELAY, dom.list);
+ index = await download(INDEX_URL, {responseType: 'json'});
+ clearTimeout(timer);
+ $.remove(':scope > .lds-spinner', dom.list);
+ return index;
+ }
+
+ async function search({retry} = {}) {
+ return retry && !calcCategory({retry})
+ ? []
+ : (index || await fetchIndex()).filter(isResultMatching).sort(comparator);
+ }
+
+ function isResultMatching(res) {
return (
- result.subcategory &&
- !processedResults.some(pr => pr.id === result.id) &&
- (category !== STYLUS_CATEGORY || /\bStylus\b/i.test(result.name + result.description)) &&
- category.split('.').includes(result.subcategory.split('.')[0])
+ res.f === 'uso' &&
+ res.c === category && (
+ category === STYLUS_CATEGORY
+ ? /\bStylus\b/.test(res.n)
+ : !query.length || query.every(isInHaystack, calcHaystack(res))
+ )
);
}
- /**
- * Fetches the JSON style object from userstyles.org (containing code, sections, updateUrl, etc).
- * Stores (caches) the JSON within the given result, to avoid unnecessary network usage.
- * Style JSON is fetched from the /styles/chrome/{id}.json endpoint.
- * @param {Object} result A search result object from userstyles.org
- * @returns {Promise