intermediate tweaks and fixes for inline search
* caching of search results and downloaded style info for one day * no prefetching of styles * only the next search results page is prefetched in unattended fashion * the "configure" button is shown only after installation * join the code in one closure, regroup and simplify some functions
This commit is contained in:
		
							parent
							
								
									adabb9aeff
								
							
						
					
					
						commit
						f4bfeea5a6
					
				
							
								
								
									
										25
									
								
								popup.html
									
									
									
									
									
								
							
							
						
						
									
										25
									
								
								popup.html
									
									
									
									
									
								
							| 
						 | 
					@ -118,6 +118,18 @@
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  </template>
 | 
					  </template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <template data-id="searchNav">
 | 
				
			||||||
 | 
					    <div>
 | 
				
			||||||
 | 
					      <button data-role="prev" i18n-title="paginationPrevious" disabled>◄</button>
 | 
				
			||||||
 | 
					      <label>
 | 
				
			||||||
 | 
					        <span data-role="page" i18n-title="paginationCurrent">-</span>
 | 
				
			||||||
 | 
					        /
 | 
				
			||||||
 | 
					        <span data-role="total" i18n-title="paginationTotal">-</span>
 | 
				
			||||||
 | 
					      </label>
 | 
				
			||||||
 | 
					      <button data-role="next" i18n-title="paginationNext" disabled>►</button>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <template data-id="emptySearchResult">
 | 
					  <template data-id="emptySearchResult">
 | 
				
			||||||
    <div class="search-result-empty"></div>
 | 
					    <div class="search-result-empty"></div>
 | 
				
			||||||
  </template>
 | 
					  </template>
 | 
				
			||||||
| 
						 | 
					@ -182,7 +194,6 @@
 | 
				
			||||||
          </label>
 | 
					          </label>
 | 
				
			||||||
        </span>
 | 
					        </span>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <div id="search-results-error" class="hidden"></div>
 | 
					 | 
				
			||||||
      <div id="write-style">
 | 
					      <div id="write-style">
 | 
				
			||||||
        <span id="write-style-for" i18n-text="writeStyleFor"></span>
 | 
					        <span id="write-style-for" i18n-text="writeStyleFor"></span>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
| 
						 | 
					@ -200,16 +211,10 @@
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <div id="search-results" class="hidden">
 | 
					  <div id="search-results" class="hidden">
 | 
				
			||||||
 | 
					    <div id="search-results-nav-top"></div>
 | 
				
			||||||
 | 
					    <div id="search-results-error" class="hidden"></div>
 | 
				
			||||||
    <div id="search-results-list"></div>
 | 
					    <div id="search-results-list"></div>
 | 
				
			||||||
    <div id="search-results-nav">
 | 
					    <div id="search-results-nav-bottom"></div>
 | 
				
			||||||
      <button id="search-results-nav-prev" i18n-title="paginationPrevious" disabled>←</button>
 | 
					 | 
				
			||||||
      <label>
 | 
					 | 
				
			||||||
        <span id="search-results-nav-current-page" i18n-title="paginationCurrent">-</span>
 | 
					 | 
				
			||||||
        /
 | 
					 | 
				
			||||||
        <span id="search-results-nav-total-pages" i18n-title="paginationTotal">-</span>
 | 
					 | 
				
			||||||
      </label>
 | 
					 | 
				
			||||||
      <button id="search-results-nav-next" i18n-title="paginationNext" disabled>→</button>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <svg xmlns="http://www.w3.org/2000/svg" style="display: none !important;">
 | 
					  <svg xmlns="http://www.w3.org/2000/svg" style="display: none !important;">
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -12,7 +12,7 @@ body.search-results-shown {
 | 
				
			||||||
  font-weight: bold;
 | 
					  font-weight: bold;
 | 
				
			||||||
  padding: 5px;
 | 
					  padding: 5px;
 | 
				
			||||||
  text-align: center;
 | 
					  text-align: center;
 | 
				
			||||||
  margin-right: 20px;
 | 
					  margin: -.5rem 0 1rem;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#search-results-list {
 | 
					#search-results-list {
 | 
				
			||||||
| 
						 | 
					@ -20,7 +20,8 @@ body.search-results-shown {
 | 
				
			||||||
  min-height: 200px;
 | 
					  min-height: 200px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.search-result, .search-result-empty {
 | 
					.search-result,
 | 
				
			||||||
 | 
					.search-result-empty {
 | 
				
			||||||
  position: relative;
 | 
					  position: relative;
 | 
				
			||||||
  padding: .75rem;
 | 
					  padding: .75rem;
 | 
				
			||||||
  min-height: 160px;
 | 
					  min-height: 160px;
 | 
				
			||||||
| 
						 | 
					@ -41,6 +42,11 @@ body.search-results-shown {
 | 
				
			||||||
  transform: scale(.75);
 | 
					  transform: scale(.75);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.search-result-fadein {
 | 
				
			||||||
 | 
					  animation: fadein 1s;
 | 
				
			||||||
 | 
					  animation-fill-mode: both;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.search-result-screenshot {
 | 
					.search-result-screenshot {
 | 
				
			||||||
  height: 140px;
 | 
					  height: 140px;
 | 
				
			||||||
  width: 100%;
 | 
					  width: 100%;
 | 
				
			||||||
| 
						 | 
					@ -182,22 +188,26 @@ body.search-results-shown {
 | 
				
			||||||
  margin: 3px;
 | 
					  margin: 3px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#search-results-nav {
 | 
					#search-results-nav-top,
 | 
				
			||||||
 | 
					#search-results-nav-bottom {
 | 
				
			||||||
  flex-direction: row;
 | 
					  flex-direction: row;
 | 
				
			||||||
  text-align: center;
 | 
					  text-align: center;
 | 
				
			||||||
  word-break: keep-all;
 | 
					  word-break: keep-all;
 | 
				
			||||||
  opacity: 1.0;
 | 
					  margin-bottom: 1rem;
 | 
				
			||||||
  margin-bottom: 10px;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#search-results-nav label {
 | 
					#search-results-nav-top button,
 | 
				
			||||||
  width: 40px;
 | 
					#search-results-nav-bottom button {
 | 
				
			||||||
  display: inline-block;
 | 
					  -webkit-appearance: none;
 | 
				
			||||||
  word-break: keep-all;
 | 
					  background: none;
 | 
				
			||||||
 | 
					  border: none;
 | 
				
			||||||
 | 
					  padding: .25rem 1rem;
 | 
				
			||||||
 | 
					  margin: 0 .5rem;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#search-results-nav button {
 | 
					#search-results-nav-top button:hover,
 | 
				
			||||||
  text-align: center;
 | 
					#search-results-nav-bottom button:hover {
 | 
				
			||||||
 | 
					  text-shadow: 0 0 2px currentColor;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#find-styles-inline-group label {
 | 
					#find-styles-inline-group label {
 | 
				
			||||||
| 
						 | 
					@ -212,7 +222,6 @@ body.search-results-shown {
 | 
				
			||||||
  -ms-user-select: none;
 | 
					  -ms-user-select: none;
 | 
				
			||||||
  user-select: none;
 | 
					  user-select: none;
 | 
				
			||||||
  pointer-events: none;
 | 
					  pointer-events: none;
 | 
				
			||||||
  opacity: 0;
 | 
					 | 
				
			||||||
  position: absolute;
 | 
					  position: absolute;
 | 
				
			||||||
  top: 0;
 | 
					  top: 0;
 | 
				
			||||||
  left: 0;
 | 
					  left: 0;
 | 
				
			||||||
| 
						 | 
					@ -220,12 +229,13 @@ body.search-results-shown {
 | 
				
			||||||
  width: 200px; /* don't change! use "transform: scale(.75)" */
 | 
					  width: 200px; /* don't change! use "transform: scale(.75)" */
 | 
				
			||||||
  height: 200px; /* don't change! use "transform: scale(.75)" */
 | 
					  height: 200px; /* don't change! use "transform: scale(.75)" */
 | 
				
			||||||
  margin: auto;
 | 
					  margin: auto;
 | 
				
			||||||
  animation: fadein 1s;
 | 
					  animation: lds-spinner 1s reverse;
 | 
				
			||||||
 | 
					  animation-fill-mode: both;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@keyframes lds-spinner {
 | 
					@keyframes lds-spinner {
 | 
				
			||||||
  0% {
 | 
					  0% {
 | 
				
			||||||
    opacity: .5;
 | 
					    opacity: .25;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  100% {
 | 
					  100% {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,60 +8,126 @@ window.addEventListener('showStyles:done', function _() {
 | 
				
			||||||
    return;
 | 
					    return;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  //region Init
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const BODY_CLASS = 'search-results-shown';
 | 
				
			||||||
 | 
					  const RESULT_ID_PREFIX = 'search-result-';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const BASE_URL = 'https://userstyles.org';
 | 
				
			||||||
 | 
					  const UPDATE_URL = 'https://update.userstyles.org/%.md5';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // normal category is just one word like 'github' or 'google'
 | 
				
			||||||
 | 
					  // but for some sites we need a fallback
 | 
				
			||||||
 | 
					  //   key: category.tld
 | 
				
			||||||
 | 
					  //   value <string>: use as category
 | 
				
			||||||
 | 
					  //   value true: fallback to search_terms
 | 
				
			||||||
 | 
					  const CATEGORY_FALLBACK = {
 | 
				
			||||||
 | 
					    'userstyles.org': 'userstyles.org',
 | 
				
			||||||
 | 
					    'last.fm': true,
 | 
				
			||||||
 | 
					    'Stylus': true,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  const RX_CATEGORY = /^(?:.*?)([^.]+)(?:\.com?)?\.(\w+)$/;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  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;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const BLANK_PIXEL_DATA = 'data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAA' +
 | 
				
			||||||
 | 
					                           'C1HAwCAAAAC0lEQVR42mOcXQ8AAbsBHLLDr5MAAAAASUVORK5CYII=';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // TODO: add -
 | 
				
			||||||
 | 
					  const CACHE_SIZE = 1e6;
 | 
				
			||||||
 | 
					  const CACHE_PREFIX = 'usoSearchCache/';
 | 
				
			||||||
 | 
					  const CACHE_DURATION = 24 * 3600e3;
 | 
				
			||||||
 | 
					  const CACHE_CLEANUP_THROTTLE = 60e3;
 | 
				
			||||||
 | 
					  const CACHE_EXCEPT_PROPS = ['css', 'discussions', 'additional_info'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let searchTotalPages;
 | 
				
			||||||
 | 
					  let searchCurrentPage = 1;
 | 
				
			||||||
 | 
					  let searchExhausted = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const processedResults = [];
 | 
				
			||||||
 | 
					  const unprocessedResults = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let loading = false;
 | 
				
			||||||
 | 
					  // Category for the active tab's URL.
 | 
				
			||||||
 | 
					  let category;
 | 
				
			||||||
 | 
					  let scrollToFirstResult = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let displayedPage = 1;
 | 
				
			||||||
 | 
					  let totalPages = 1;
 | 
				
			||||||
 | 
					  let totalResults = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // fade-in when the entry took that long to replace its placeholder
 | 
				
			||||||
 | 
					  const FADEIN_THRESHOLD = 50;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const dom = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Object.assign($('#find-styles-link'), {
 | 
					  Object.assign($('#find-styles-link'), {
 | 
				
			||||||
    href: searchUserstyles().getSearchPageURL(tabURL),
 | 
					    href: getSearchPageURL(tabURL),
 | 
				
			||||||
    onclick(event) {
 | 
					    onclick(event) {
 | 
				
			||||||
      if (!prefs.get('popup.findStylesInline')) {
 | 
					      if (!prefs.get('popup.findStylesInline') || dom.container) {
 | 
				
			||||||
        handleEvent.openURLandHide.call(this, event);
 | 
					        handleEvent.openURLandHide.call(this, event);
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      $('#find-styles-inline-group').classList.add('hidden');
 | 
					      $('#find-styles-inline-group').classList.add('hidden');
 | 
				
			||||||
 | 
					      this.textContent = this.title;
 | 
				
			||||||
 | 
					      this.title = '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const searchResults = searchResultsController();
 | 
					      init();
 | 
				
			||||||
      searchResults.init();
 | 
					      load();
 | 
				
			||||||
      searchResults.load();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      event.preventDefault();
 | 
					      event.preventDefault();
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  return;
 | 
				
			||||||
   * Represents the search results within the Stylus popup.
 | 
					 | 
				
			||||||
   * @returns {Object} Includes load(), next(), and prev() methods to alter the search results.
 | 
					 | 
				
			||||||
   */
 | 
					 | 
				
			||||||
  function searchResultsController() {
 | 
					 | 
				
			||||||
    const DISPLAYED_RESULTS_PER_PAGE = 10; // Number of results to display in popup.html
 | 
					 | 
				
			||||||
    const DELAY_AFTER_FETCHING_STYLES = 0; // Millisecs to wait before fetching next batch of search results.
 | 
					 | 
				
			||||||
    const DELAY_BEFORE_SEARCHING_STYLES = 0; // Millisecs to wait before fetching .JSON for next search result.
 | 
					 | 
				
			||||||
    const searchAPI = searchUserstyles();
 | 
					 | 
				
			||||||
    const unprocessedResults = []; // Search results not yet processed.
 | 
					 | 
				
			||||||
    const processedResults = []; // Search results that are not installed and apply ot the page (includes 'json' field with full style).
 | 
					 | 
				
			||||||
    const BLANK_PIXEL_DATA = 'data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAA' +
 | 
					 | 
				
			||||||
                             'C1HAwCAAAAC0lEQVR42mOcXQ8AAbsBHLLDr5MAAAAASUVORK5CYII=';
 | 
					 | 
				
			||||||
    const UPDATE_URL = 'https://update.userstyles.org/%.md5';
 | 
					 | 
				
			||||||
    const ENTRY_ID_PREFIX = 'search-result-';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let scrollToFirstResult = true;
 | 
					 | 
				
			||||||
    let loading = false;
 | 
					 | 
				
			||||||
    let category; // Category for the active tab's URL.
 | 
					 | 
				
			||||||
    let currentDisplayedPage = 1; // Current page number in popup.html
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return {init, load, next, prev};
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  function init() {
 | 
					  function init() {
 | 
				
			||||||
      $('#search-results-nav-prev').onclick = prev;
 | 
					    document.body.classList.add(BODY_CLASS);
 | 
				
			||||||
      $('#search-results-nav-next').onclick = next;
 | 
					
 | 
				
			||||||
      document.body.classList.add('search-results-shown');
 | 
					    dom.container = $('#search-results');
 | 
				
			||||||
 | 
					    dom.error = $('#search-results-error');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dom.nav = {};
 | 
				
			||||||
 | 
					    const navOnClick = {prev, next};
 | 
				
			||||||
 | 
					    for (const place of ['top', 'bottom']) {
 | 
				
			||||||
 | 
					      const nav = $('#search-results-nav-' + place);
 | 
				
			||||||
 | 
					      nav.appendChild(template.searchNav.cloneNode(true));
 | 
				
			||||||
 | 
					      dom.nav[place] = nav;
 | 
				
			||||||
 | 
					      for (const child of $$('[data-role]', nav)) {
 | 
				
			||||||
 | 
					        const role = child.dataset.role;
 | 
				
			||||||
 | 
					        child.onclick = navOnClick[role];
 | 
				
			||||||
 | 
					        nav['_' + role] = child;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dom.list = $('#search-results-list');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    addEventListener('styleDeleted', ({detail}) => {
 | 
					    addEventListener('styleDeleted', ({detail}) => {
 | 
				
			||||||
        const entries = [...$('#search-results-list').children];
 | 
					      const entries = [...dom.list.children];
 | 
				
			||||||
      const entry = entries.find(el => el._result.installedStyleId === detail.id);
 | 
					      const entry = entries.find(el => el._result.installedStyleId === detail.id);
 | 
				
			||||||
      if (entry) {
 | 
					      if (entry) {
 | 
				
			||||||
        entry._result.installed = false;
 | 
					        entry._result.installed = false;
 | 
				
			||||||
        renderActionButtons(entry);
 | 
					        renderActionButtons(entry);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    addEventListener('styleAdded', ({detail: {style: {md5Url}}}) => {
 | 
				
			||||||
 | 
					      const usoId = md5Url && md5Url.match(/\d+|$/)[0];
 | 
				
			||||||
 | 
					      const entry = usoId && $('#' + RESULT_ID_PREFIX + usoId);
 | 
				
			||||||
 | 
					      if (entry) {
 | 
				
			||||||
 | 
					        entry._result.installed = true;
 | 
				
			||||||
 | 
					        renderActionButtons(entry);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  //endregion
 | 
				
			||||||
 | 
					  //region Loader
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Sets loading status of search results.
 | 
					   * Sets loading status of search results.
 | 
				
			||||||
| 
						 | 
					@ -70,14 +136,8 @@ window.addEventListener('showStyles:done', function _() {
 | 
				
			||||||
  function setLoading(isLoading) {
 | 
					  function setLoading(isLoading) {
 | 
				
			||||||
    if (loading !== isLoading) {
 | 
					    if (loading !== isLoading) {
 | 
				
			||||||
      loading = isLoading;
 | 
					      loading = isLoading;
 | 
				
			||||||
 | 
					      // Refresh elements that depend on `loading` state.
 | 
				
			||||||
        render(); // Refresh elements that depend on `loading` state.
 | 
					      render();
 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (isLoading) {
 | 
					 | 
				
			||||||
          showSpinner('#search-results');
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
          $.remove('#search-results > .lds-spinner');
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -87,75 +147,13 @@ window.addEventListener('showStyles:done', function _() {
 | 
				
			||||||
      new Array(12).fill($create('div')).map(e => e.cloneNode())));
 | 
					      new Array(12).fill($create('div')).map(e => e.cloneNode())));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function render() {
 | 
					  /** Increments displayedPage and loads results. */
 | 
				
			||||||
      let startIndex = (currentDisplayedPage - 1) * DISPLAYED_RESULTS_PER_PAGE;
 | 
					 | 
				
			||||||
      const endIndex = currentDisplayedPage * DISPLAYED_RESULTS_PER_PAGE;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      const list = $('#search-results-list');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // keep rendered elements with ids in the range of interest
 | 
					 | 
				
			||||||
      for (let i = 0; i < DISPLAYED_RESULTS_PER_PAGE;) {
 | 
					 | 
				
			||||||
        const el = list.children[i];
 | 
					 | 
				
			||||||
        if (!el) {
 | 
					 | 
				
			||||||
          break;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if (el.id === 'search-result-' + (processedResults[startIndex] || {}).id) {
 | 
					 | 
				
			||||||
          startIndex++;
 | 
					 | 
				
			||||||
          i++;
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
          el.remove();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      const displayedResults = processedResults.slice(startIndex, endIndex);
 | 
					 | 
				
			||||||
      displayedResults.forEach(createSearchResultNode);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      $('#search-results-nav-prev').disabled = (currentDisplayedPage <= 1 || loading);
 | 
					 | 
				
			||||||
      $('#search-results-nav-current-page').textContent = currentDisplayedPage;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      let totalResultsCount = processedResults.length;
 | 
					 | 
				
			||||||
      if (unprocessedResults.length > 0) {
 | 
					 | 
				
			||||||
        // Add 1 page if there's results left to process.
 | 
					 | 
				
			||||||
        totalResultsCount += DISPLAYED_RESULTS_PER_PAGE;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      const totalPageCount = Math.ceil(Math.max(1, totalResultsCount / DISPLAYED_RESULTS_PER_PAGE));
 | 
					 | 
				
			||||||
      $('#search-results-nav-next').disabled = (currentDisplayedPage >= totalPageCount || loading);
 | 
					 | 
				
			||||||
      $('#search-results-nav-total-pages').textContent = totalPageCount;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // Fill in remaining search results with blank results + spinners
 | 
					 | 
				
			||||||
      const maxResults = currentDisplayedPage < totalPageCount
 | 
					 | 
				
			||||||
        ? DISPLAYED_RESULTS_PER_PAGE
 | 
					 | 
				
			||||||
        : displayedResults.length + unprocessedResults.length;
 | 
					 | 
				
			||||||
      for (let i = list.children.length; i < maxResults; i++) {
 | 
					 | 
				
			||||||
        const entry = template.emptySearchResult.cloneNode(true);
 | 
					 | 
				
			||||||
        list.appendChild(entry);
 | 
					 | 
				
			||||||
        showSpinner(entry);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (scrollToFirstResult && list.children[0]) {
 | 
					 | 
				
			||||||
        scrollToFirstResult = false;
 | 
					 | 
				
			||||||
        if (!FIREFOX || FIREFOX >= 55) {
 | 
					 | 
				
			||||||
          list.children[0].scrollIntoView({behavior: 'smooth', block: 'start'});
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * @returns {Boolean} If we should process more results.
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    function shouldLoadMore() {
 | 
					 | 
				
			||||||
      return (processedResults.length < currentDisplayedPage * DISPLAYED_RESULTS_PER_PAGE);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    function loadMoreIfNeeded() {
 | 
					 | 
				
			||||||
      if (shouldLoadMore()) {
 | 
					 | 
				
			||||||
        setTimeout(load, DELAY_BEFORE_SEARCHING_STYLES);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /** Increments currentDisplayedPage and loads results. */
 | 
					 | 
				
			||||||
  function next() {
 | 
					  function next() {
 | 
				
			||||||
      currentDisplayedPage += 1;
 | 
					    if (loading) {
 | 
				
			||||||
 | 
					      debounce(next, 100);
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    displayedPage += 1;
 | 
				
			||||||
    scrollToFirstResult = true;
 | 
					    scrollToFirstResult = true;
 | 
				
			||||||
    render();
 | 
					    render();
 | 
				
			||||||
    loadMoreIfNeeded();
 | 
					    loadMoreIfNeeded();
 | 
				
			||||||
| 
						 | 
					@ -163,7 +161,11 @@ window.addEventListener('showStyles:done', function _() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /** Decrements currentPage and loads results. */
 | 
					  /** Decrements currentPage and loads results. */
 | 
				
			||||||
  function prev() {
 | 
					  function prev() {
 | 
				
			||||||
      currentDisplayedPage = Math.max(1, currentDisplayedPage - 1);
 | 
					    if (loading) {
 | 
				
			||||||
 | 
					      debounce(next, 100);
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    displayedPage = Math.max(1, displayedPage - 1);
 | 
				
			||||||
    scrollToFirstResult = true;
 | 
					    scrollToFirstResult = true;
 | 
				
			||||||
    render();
 | 
					    render();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					@ -180,43 +182,58 @@ window.addEventListener('showStyles:done', function _() {
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      message = 'Error loading search results: ' + reason;
 | 
					      message = 'Error loading search results: ' + reason;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
      $('#search-results').classList.add('hidden');
 | 
					    dom.error.textContent = message;
 | 
				
			||||||
      $('#search-results-error').textContent = message;
 | 
					    dom.error.classList.remove('hidden');
 | 
				
			||||||
      $('#search-results-error').classList.remove('hidden');
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Initializes search results container, starts fetching results.
 | 
					   * Initializes search results container, starts fetching results.
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  function load() {
 | 
					  function load() {
 | 
				
			||||||
      if (unprocessedResults.length > 0) {
 | 
					    if (searchExhausted) {
 | 
				
			||||||
        // Keep processing search results if there are any.
 | 
					      if (!processedResults.length) {
 | 
				
			||||||
        processNextResult();
 | 
					 | 
				
			||||||
      } else if (searchAPI.isExhausted()) {
 | 
					 | 
				
			||||||
        // Stop if no more search results.
 | 
					 | 
				
			||||||
        if (processedResults.length === 0) {
 | 
					 | 
				
			||||||
          // No results
 | 
					 | 
				
			||||||
        error(404);
 | 
					        error(404);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      } else {
 | 
					      return;
 | 
				
			||||||
        setLoading(true);
 | 
					 | 
				
			||||||
        // Search for more results.
 | 
					 | 
				
			||||||
        $('#search-results').classList.remove('hidden');
 | 
					 | 
				
			||||||
        $('#search-results-error').classList.add('hidden');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Discover "category" for the URL, then search.
 | 
					 | 
				
			||||||
        category = searchAPI.getCategory(tabURL);
 | 
					 | 
				
			||||||
        searchAPI.search(category)
 | 
					 | 
				
			||||||
          .then(searchResults => {
 | 
					 | 
				
			||||||
            setLoading(false);
 | 
					 | 
				
			||||||
            if (searchResults.data.length === 0) {
 | 
					 | 
				
			||||||
              throw 404;
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
            unprocessedResults.push(unprocessedResults, ...searchResults.data);
 | 
					
 | 
				
			||||||
 | 
					    setLoading(true);
 | 
				
			||||||
 | 
					    dom.container.classList.remove('hidden');
 | 
				
			||||||
 | 
					    dom.error.classList.add('hidden');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let pass = category ? 1 : 0;
 | 
				
			||||||
 | 
					    category = category || getCategory();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    search({category})
 | 
				
			||||||
 | 
					      .then(function process(results) {
 | 
				
			||||||
 | 
					        const data = results.data.filter(sameCategory);
 | 
				
			||||||
 | 
					        pass++;
 | 
				
			||||||
 | 
					        if (pass === 1 && !data.length) {
 | 
				
			||||||
 | 
					          category = getCategory({keepTLD: true});
 | 
				
			||||||
 | 
					          return search({category, restart: true}).then(process);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        const numIrrelevant = results.data.length - data.length;
 | 
				
			||||||
 | 
					        totalResults = results.current_page === 1 ? results.total_entries : totalResults;
 | 
				
			||||||
 | 
					        totalResults = Math.max(0, totalResults - numIrrelevant);
 | 
				
			||||||
 | 
					        totalPages = Math.ceil(totalResults / DISPLAY_PER_PAGE);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (data.length) {
 | 
				
			||||||
 | 
					          setLoading(false);
 | 
				
			||||||
 | 
					          unprocessedResults.push(...data);
 | 
				
			||||||
          processNextResult();
 | 
					          processNextResult();
 | 
				
			||||||
 | 
					        } else if (numIrrelevant) {
 | 
				
			||||||
 | 
					          load();
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          return Promise.reject(404);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
      .catch(error);
 | 
					      .catch(error);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function loadMoreIfNeeded() {
 | 
				
			||||||
 | 
					    if (processedResults.length < (displayedPage + 1) * DISPLAY_PER_PAGE) {
 | 
				
			||||||
 | 
					      setTimeout(load, DELAY_BEFORE_SEARCHING_STYLES);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
| 
						 | 
					@ -226,44 +243,94 @@ window.addEventListener('showStyles:done', function _() {
 | 
				
			||||||
   * Recurses until shouldLoadMore() is false.
 | 
					   * Recurses until shouldLoadMore() is false.
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  function processNextResult() {
 | 
					  function processNextResult() {
 | 
				
			||||||
      if (!shouldLoadMore()) {
 | 
					    const result = unprocessedResults.shift();
 | 
				
			||||||
        return;
 | 
					    if (!result) {
 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (unprocessedResults.length === 0) {
 | 
					 | 
				
			||||||
        // No more results to process
 | 
					 | 
				
			||||||
      loadMoreIfNeeded();
 | 
					      loadMoreIfNeeded();
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    const md5Url = UPDATE_URL.replace('%', result.id);
 | 
				
			||||||
      // Process the next result in the queue.
 | 
					    getStylesSafe({md5Url}).then(([installedStyle]) => {
 | 
				
			||||||
      const nextResult = unprocessedResults.shift();
 | 
					 | 
				
			||||||
      getStylesSafe({md5Url: UPDATE_URL.replace('%', nextResult.id)})
 | 
					 | 
				
			||||||
        .then(([installedStyle]) => {
 | 
					 | 
				
			||||||
      if (installedStyle) {
 | 
					      if (installedStyle) {
 | 
				
			||||||
            setTimeout(processNextResult);
 | 
					        totalResults = Math.max(0, totalResults - 1);
 | 
				
			||||||
            return;
 | 
					      } else {
 | 
				
			||||||
          }
 | 
					        processedResults.push(result);
 | 
				
			||||||
          if (nextResult.category !== 'site') {
 | 
					 | 
				
			||||||
            setTimeout(processNextResult);
 | 
					 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          // Style not installed.
 | 
					 | 
				
			||||||
          // Get "style_settings" (customizations)
 | 
					 | 
				
			||||||
          searchAPI.fetchStyle(nextResult.id)
 | 
					 | 
				
			||||||
            .then(userstyleObject => {
 | 
					 | 
				
			||||||
              // Store style settings for detecting customization later.
 | 
					 | 
				
			||||||
              nextResult.style_settings = userstyleObject.style_settings;
 | 
					 | 
				
			||||||
              processedResults.push(nextResult);
 | 
					 | 
				
			||||||
        render();
 | 
					        render();
 | 
				
			||||||
              setTimeout(processNextResult, DELAY_AFTER_FETCHING_STYLES);
 | 
					      }
 | 
				
			||||||
            })
 | 
					      setTimeout(processNextResult, !installedStyle && DELAY_AFTER_FETCHING_STYLES);
 | 
				
			||||||
            .catch(reason => {
 | 
					 | 
				
			||||||
              console.log('processNextResult(', nextResult.id, ') => [ERROR]: ', reason);
 | 
					 | 
				
			||||||
              setTimeout(processNextResult, DELAY_AFTER_FETCHING_STYLES);
 | 
					 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  //endregion
 | 
				
			||||||
 | 
					  //region UI
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function render() {
 | 
				
			||||||
 | 
					    let start = (displayedPage - 1) * DISPLAY_PER_PAGE;
 | 
				
			||||||
 | 
					    const end = displayedPage * DISPLAY_PER_PAGE;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					      slot = slot.nextElementSibling;
 | 
				
			||||||
 | 
					      plantAt++;
 | 
				
			||||||
 | 
					      start++;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const plantEntry = entry => {
 | 
				
			||||||
 | 
					      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++;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (const place in dom.nav) {
 | 
				
			||||||
 | 
					      const nav = dom.nav[place];
 | 
				
			||||||
 | 
					      nav._prev.disabled = displayedPage <= 1;
 | 
				
			||||||
 | 
					      nav._next.disabled = displayedPage >= totalPages;
 | 
				
			||||||
 | 
					      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++;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    while (dom.list.children.length > maxResults) {
 | 
				
			||||||
 | 
					      dom.list.lastElementChild.remove();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (scrollToFirstResult && dom.list.children[0]) {
 | 
				
			||||||
 | 
					      scrollToFirstResult = false;
 | 
				
			||||||
 | 
					      if (!FIREFOX || FIREFOX >= 55) {
 | 
				
			||||||
 | 
					        setTimeout(() => {
 | 
				
			||||||
 | 
					          dom.container.scrollIntoView({behavior: 'smooth', block: 'start'});
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Constructs and adds the given search result to the popup's Search Results container.
 | 
					   * Constructs and adds the given search result to the popup's Search Results container.
 | 
				
			||||||
| 
						 | 
					@ -287,26 +354,32 @@ window.addEventListener('showStyles:done', function _() {
 | 
				
			||||||
    const entry = template.searchResult.cloneNode(true);
 | 
					    const entry = template.searchResult.cloneNode(true);
 | 
				
			||||||
    Object.assign(entry, {
 | 
					    Object.assign(entry, {
 | 
				
			||||||
      _result: result,
 | 
					      _result: result,
 | 
				
			||||||
        id: ENTRY_ID_PREFIX + result.id,
 | 
					      id: RESULT_ID_PREFIX + result.id,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Object.assign($('.search-result-title', entry), {
 | 
					    Object.assign($('.search-result-title', entry), {
 | 
				
			||||||
      onclick: handleEvent.openURLandHide,
 | 
					      onclick: handleEvent.openURLandHide,
 | 
				
			||||||
        href: searchAPI.BASE_URL + result.url
 | 
					      href: BASE_URL + result.url
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const displayedName = result.name.length < 300 ? result.name : result.name.slice(0, 300) + '...';
 | 
					    const displayedName = result.name.length < 300 ? result.name : result.name.slice(0, 300) + '...';
 | 
				
			||||||
    $('.search-result-title span', entry).textContent = tWordBreak(displayedName);
 | 
					    $('.search-result-title span', entry).textContent = tWordBreak(displayedName);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const screenshot = $('.search-result-screenshot', entry);
 | 
					    const screenshot = $('.search-result-screenshot', entry);
 | 
				
			||||||
      let screenshotUrl = result.screenshot_url;
 | 
					    let url = result.screenshot_url;
 | 
				
			||||||
      if (screenshotUrl === null) {
 | 
					    if (!url) {
 | 
				
			||||||
        screenshotUrl = BLANK_PIXEL_DATA;
 | 
					      url = BLANK_PIXEL_DATA;
 | 
				
			||||||
      screenshot.classList.add('no-screenshot');
 | 
					      screenshot.classList.add('no-screenshot');
 | 
				
			||||||
      } else if (RegExp(/^[0-9]*_after.(jpe?g|png|gif)$/i).test(screenshotUrl)) {
 | 
					    } else if (/^[0-9]*_after.(jpe?g|png|gif)$/i.test(url)) {
 | 
				
			||||||
        screenshotUrl = searchAPI.BASE_URL + '/style_screenshot_thumbnails/' + screenshotUrl;
 | 
					      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');
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
      screenshot.src = screenshotUrl;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const description = result.description
 | 
					    const description = result.description
 | 
				
			||||||
      .replace(/<[^>]*>/g, '')
 | 
					      .replace(/<[^>]*>/g, '')
 | 
				
			||||||
| 
						 | 
					@ -319,7 +392,7 @@ window.addEventListener('showStyles:done', function _() {
 | 
				
			||||||
    Object.assign($('.search-result-author-link', entry), {
 | 
					    Object.assign($('.search-result-author-link', entry), {
 | 
				
			||||||
      textContent: result.user.name,
 | 
					      textContent: result.user.name,
 | 
				
			||||||
      title: result.user.name,
 | 
					      title: result.user.name,
 | 
				
			||||||
        href: searchAPI.BASE_URL + '/users/' + result.user.id,
 | 
					      href: BASE_URL + '/users/' + result.user.id,
 | 
				
			||||||
      onclick(event) {
 | 
					      onclick(event) {
 | 
				
			||||||
        event.stopPropagation();
 | 
					        event.stopPropagation();
 | 
				
			||||||
        handleEvent.openURLandHide.call(this, event);
 | 
					        handleEvent.openURLandHide.call(this, event);
 | 
				
			||||||
| 
						 | 
					@ -349,9 +422,8 @@ window.addEventListener('showStyles:done', function _() {
 | 
				
			||||||
    Object.assign($('.search-result-install-count', entry), {
 | 
					    Object.assign($('.search-result-install-count', entry), {
 | 
				
			||||||
      textContent: result.total_install_count.toLocaleString()
 | 
					      textContent: result.total_install_count.toLocaleString()
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
      renderActionButtons(entry);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      $('#search-results-list').appendChild(entry);
 | 
					    renderActionButtons(entry);
 | 
				
			||||||
    return entry;
 | 
					    return entry;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -363,13 +435,13 @@ window.addEventListener('showStyles:done', function _() {
 | 
				
			||||||
    installButton.onclick = onInstallClicked;
 | 
					    installButton.onclick = onInstallClicked;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const result = entry._result;
 | 
					    const result = entry._result;
 | 
				
			||||||
      if (result.style_settings.length > 0) {
 | 
					    if ((result.style_settings || []).length > 0) {
 | 
				
			||||||
      // Style has customizations
 | 
					      // Style has customizations
 | 
				
			||||||
      installButton.classList.add('customize');
 | 
					      installButton.classList.add('customize');
 | 
				
			||||||
      uninstallButton.classList.add('customize');
 | 
					      uninstallButton.classList.add('customize');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const customizeButton = $('.search-result-customize', entry);
 | 
					      const customizeButton = $('.search-result-customize', entry);
 | 
				
			||||||
        customizeButton.dataset.href = searchAPI.BASE_URL + result.url;
 | 
					      customizeButton.dataset.href = BASE_URL + result.url;
 | 
				
			||||||
      customizeButton.dataset.sendMessage = JSON.stringify({method: 'openSettings'});
 | 
					      customizeButton.dataset.sendMessage = JSON.stringify({method: 'openSettings'});
 | 
				
			||||||
      customizeButton.classList.remove('hidden');
 | 
					      customizeButton.classList.remove('hidden');
 | 
				
			||||||
      customizeButton.onclick = function (event) {
 | 
					      customizeButton.onclick = function (event) {
 | 
				
			||||||
| 
						 | 
					@ -404,91 +476,80 @@ window.addEventListener('showStyles:done', function _() {
 | 
				
			||||||
    showSpinner(entry);
 | 
					    showSpinner(entry);
 | 
				
			||||||
    installButton.disabled = true;
 | 
					    installButton.disabled = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // Fetch .JSON style
 | 
					    // Fetch settings to see if we should display "configure" button
 | 
				
			||||||
      searchAPI.fetchStyleJson(result)
 | 
					    Promise.all([
 | 
				
			||||||
        .then(userstyleJson => {
 | 
					      fetchStyleJson(result),
 | 
				
			||||||
          userstyleJson.reason = 'install';
 | 
					      fetchStyleSettings(result),
 | 
				
			||||||
          if (result.style_settings.length) {
 | 
					    ])
 | 
				
			||||||
            // this will make the popup show a config-on-homepage icon
 | 
					    .then(([style, settings]) => {
 | 
				
			||||||
            userstyleJson.updateUrl += '?';
 | 
					      // show a 'config-on-homepage' icon in the popup
 | 
				
			||||||
          }
 | 
					      style.updateUrl += settings.length ? '?' : '';
 | 
				
			||||||
          // Install style
 | 
					      // show a 'style installed' tooltip in the manager
 | 
				
			||||||
          saveStyleSafe(userstyleJson)
 | 
					      style.reason = 'install';
 | 
				
			||||||
 | 
					      return saveStyleSafe(style);
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
    .then(savedStyle => {
 | 
					    .then(savedStyle => {
 | 
				
			||||||
              // Success: Store installed styleId, mark as installed.
 | 
					 | 
				
			||||||
      result.installed = true;
 | 
					      result.installed = true;
 | 
				
			||||||
      result.installedStyleId = savedStyle.id;
 | 
					      result.installedStyleId = savedStyle.id;
 | 
				
			||||||
      renderActionButtons(entry);
 | 
					      renderActionButtons(entry);
 | 
				
			||||||
 | 
					 | 
				
			||||||
              $.remove('.lds-spinner', entry);
 | 
					 | 
				
			||||||
              installButton.disabled = false;
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
    .catch(reason => {
 | 
					    .catch(reason => {
 | 
				
			||||||
      const usoId = result.id;
 | 
					      const usoId = result.id;
 | 
				
			||||||
          console.log('install:saveStyleSafe(usoID:', usoId, ') => [ERROR]: ', reason);
 | 
					      console.debug('install:saveStyleSafe(usoID:', usoId, ') => [ERROR]: ', reason);
 | 
				
			||||||
      alert('Error while downloading usoID:' + usoId + '\nReason: ' + reason);
 | 
					      alert('Error while downloading usoID:' + usoId + '\nReason: ' + reason);
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .then(() => {
 | 
				
			||||||
      $.remove('.lds-spinner', entry);
 | 
					      $.remove('.lds-spinner', entry);
 | 
				
			||||||
      installButton.disabled = false;
 | 
					      installButton.disabled = false;
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
      return true;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  } // End of searchResultsController
 | 
					    function fetchStyleSettings(result) {
 | 
				
			||||||
 | 
					      return result.style_settings ||
 | 
				
			||||||
 | 
					        fetchStyle(result.id).then(style => {
 | 
				
			||||||
 | 
					          result.style_settings = style.style_settings || [];
 | 
				
			||||||
 | 
					          return result.style_settings;
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * Library for interacting with userstyles.org
 | 
					 | 
				
			||||||
 * @returns {Object} Exposed methods representing the search results on userstyles.org
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
function searchUserstyles() {
 | 
					 | 
				
			||||||
  const BASE_URL = 'https://userstyles.org';
 | 
					 | 
				
			||||||
  const CACHE_PREFIX = 'usoSearchCache';
 | 
					 | 
				
			||||||
  const CACHE_DURATION = 1 * 3600e3;
 | 
					 | 
				
			||||||
  let totalPages;
 | 
					 | 
				
			||||||
  let currentPage = 1;
 | 
					 | 
				
			||||||
  let exhausted = false;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return {BASE_URL, getCategory, getSearchPageURL, isExhausted, search, fetchStyleJson, fetchStyle};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /**
 | 
					 | 
				
			||||||
   * @returns {Boolean} If there are no more results to fetch from userstyles.org
 | 
					 | 
				
			||||||
   */
 | 
					 | 
				
			||||||
  function isExhausted() {
 | 
					 | 
				
			||||||
    return exhausted;
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  function getSearchPageURL(url) {
 | 
					  //endregion
 | 
				
			||||||
    const category = getCategory(url);
 | 
					  //region USO API wrapper
 | 
				
			||||||
    if (category === 'STYLUS') {
 | 
					
 | 
				
			||||||
      return BASE_URL + '/styles/browse/?search_terms=Stylus';
 | 
					  function getSearchPageURL() {
 | 
				
			||||||
    } else {
 | 
					    const category = getCategory();
 | 
				
			||||||
      return BASE_URL + '/styles/browse/' + category;
 | 
					    return BASE_URL +
 | 
				
			||||||
    }
 | 
					      '/styles/browse/' +
 | 
				
			||||||
 | 
					      (category in CATEGORY_FALLBACK ? '?search_terms=' : '') +
 | 
				
			||||||
 | 
					      category;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Resolves the Userstyles.org "category" for a given URL.
 | 
					   * Resolves the Userstyles.org "category" for a given URL.
 | 
				
			||||||
   * @param {String} url The URL to a webpage.
 | 
					 | 
				
			||||||
   * @returns {Promise<String>} The category for a URL, or the hostname if category is not found.
 | 
					 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  function getCategory(url) {
 | 
					  function getCategory({keepTLD} = {}) {
 | 
				
			||||||
    const u = tryCatch(() => new URL(url));
 | 
					    const u = tryCatch(() => new URL(tabURL));
 | 
				
			||||||
    if (!u) {
 | 
					    if (!u) {
 | 
				
			||||||
      return ''; // Invalid URL
 | 
					      // Invalid URL
 | 
				
			||||||
 | 
					      return '';
 | 
				
			||||||
    } else if (u.protocol === 'file:') {
 | 
					    } else if (u.protocol === 'file:') {
 | 
				
			||||||
      return 'file:'; // File page
 | 
					      return 'file:';
 | 
				
			||||||
    } else if (u.protocol === location.protocol) {
 | 
					    } else if (u.protocol === location.protocol) {
 | 
				
			||||||
      return 'STYLUS'; // Stylus page
 | 
					      return 'Stylus';
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      // Website address, strip TLD & subdomain
 | 
					      // Website address, strip TLD & subdomain
 | 
				
			||||||
      let domain = u.hostname.replace(/^www\.|(\.com?)?\.\w+$/g, '').split('.').pop();
 | 
					      const [, category = u.hostname, tld = ''] = u.hostname.match(RX_CATEGORY) || [];
 | 
				
			||||||
      if (domain === 'userstyles') {
 | 
					      const categoryWithTLD = category + '.' + tld;
 | 
				
			||||||
        domain = 'userstyles.org';
 | 
					      const fallback = CATEGORY_FALLBACK[categoryWithTLD];
 | 
				
			||||||
 | 
					      return fallback === true && categoryWithTLD || fallback || category + (keepTLD ? tld : '');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
      return domain;
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function sameCategory(result) {
 | 
				
			||||||
 | 
					    return result.subcategory && (
 | 
				
			||||||
 | 
					      category === result.subcategory ||
 | 
				
			||||||
 | 
					      category === 'Stylus' && /^(chrome|moz)-extension$/.test(result.subcategory) ||
 | 
				
			||||||
 | 
					      category.replace('.', '').toLowerCase() === result.subcategory.replace('.', '').toLowerCase()
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
| 
						 | 
					@ -522,8 +583,7 @@ function searchUserstyles() {
 | 
				
			||||||
   * @returns {Promise<Object>} An object containing info about the style, e.g. name, author, etc.
 | 
					   * @returns {Promise<Object>} An object containing info about the style, e.g. name, author, etc.
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  function fetchStyle(userstylesId) {
 | 
					  function fetchStyle(userstylesId) {
 | 
				
			||||||
    return readCache(userstylesId)
 | 
					    return readCache(userstylesId).then(json => json ||
 | 
				
			||||||
      .then(json => json ||
 | 
					 | 
				
			||||||
      download(BASE_URL + '/api/v1/styles/' + userstylesId, {
 | 
					      download(BASE_URL + '/api/v1/styles/' + userstylesId, {
 | 
				
			||||||
        method: 'GET',
 | 
					        method: 'GET',
 | 
				
			||||||
        headers: {
 | 
					        headers: {
 | 
				
			||||||
| 
						 | 
					@ -532,27 +592,37 @@ function searchUserstyles() {
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        responseType: 'json',
 | 
					        responseType: 'json',
 | 
				
			||||||
        body: null
 | 
					        body: null
 | 
				
			||||||
        }).then(writeCache));
 | 
					      }).then(json => {
 | 
				
			||||||
 | 
					        for (const prop of CACHE_EXCEPT_PROPS) {
 | 
				
			||||||
 | 
					          delete json[prop];
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        writeCache(json);
 | 
				
			||||||
 | 
					        return json;
 | 
				
			||||||
 | 
					      }));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Fetches (and JSON-parses) search results from a userstyles.org search API.
 | 
					   * Fetches (and JSON-parses) search results from a userstyles.org search API.
 | 
				
			||||||
   * Automatically sets currentPage and totalPages.
 | 
					   * Automatically sets searchCurrentPage and searchTotalPages.
 | 
				
			||||||
   * @param {string} category The usrestyles.org "category" (subcategory) OR a any search string.
 | 
					   * @param {string} category The usrestyles.org "category" (subcategory) OR a any search string.
 | 
				
			||||||
   * @return {Object} Response object from userstyles.org
 | 
					   * @return {Object} Response object from userstyles.org
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  function search(category) {
 | 
					  function search({category, restart}) {
 | 
				
			||||||
    if (totalPages !== undefined && currentPage > totalPages) {
 | 
					    if (restart) {
 | 
				
			||||||
 | 
					      searchCurrentPage = 1;
 | 
				
			||||||
 | 
					      searchTotalPages = undefined;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (searchTotalPages !== undefined && searchCurrentPage > searchTotalPages) {
 | 
				
			||||||
      return Promise.resolve({'data':[]});
 | 
					      return Promise.resolve({'data':[]});
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const searchURL = BASE_URL +
 | 
					    const searchURL = BASE_URL +
 | 
				
			||||||
      '/api/v1/styles/subcategory' +
 | 
					      '/api/v1/styles/subcategory' +
 | 
				
			||||||
      '?search=' + encodeURIComponent(category) +
 | 
					      '?search=' + encodeURIComponent(category) +
 | 
				
			||||||
      '&page=' + currentPage +
 | 
					      '&page=' + searchCurrentPage +
 | 
				
			||||||
      '&country=NA';
 | 
					      '&country=NA';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const cacheKey = category + '/' + currentPage;
 | 
					    const cacheKey = category + '/' + searchCurrentPage;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return readCache(cacheKey)
 | 
					    return readCache(cacheKey)
 | 
				
			||||||
      .then(json => json ||
 | 
					      .then(json => json ||
 | 
				
			||||||
| 
						 | 
					@ -570,55 +640,84 @@ function searchUserstyles() {
 | 
				
			||||||
          return json;
 | 
					          return json;
 | 
				
			||||||
        }))
 | 
					        }))
 | 
				
			||||||
      .then(json => {
 | 
					      .then(json => {
 | 
				
			||||||
        currentPage = json.current_page + 1;
 | 
					        searchCurrentPage = json.current_page + 1;
 | 
				
			||||||
        totalPages = json.total_pages;
 | 
					        searchTotalPages = json.total_pages;
 | 
				
			||||||
        exhausted = (currentPage > totalPages);
 | 
					        searchExhausted = (searchCurrentPage > searchTotalPages);
 | 
				
			||||||
        return json;
 | 
					        return json;
 | 
				
			||||||
      }).catch(reason => {
 | 
					      }).catch(reason => {
 | 
				
			||||||
        exhausted = true;
 | 
					        searchExhausted = true;
 | 
				
			||||||
        return Promise.reject(reason);
 | 
					        return Promise.reject(reason);
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  //endregion
 | 
				
			||||||
 | 
					  //region Cache
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  function readCache(id) {
 | 
					  function readCache(id) {
 | 
				
			||||||
    return BG.chromeLocal.getLZValue(CACHE_PREFIX + id).then(data => {
 | 
					    const key = CACHE_PREFIX + id;
 | 
				
			||||||
      if (!data || Date.now() - data.cacheWriteDate < CACHE_DURATION) {
 | 
					    return BG.chromeLocal.getValue(key).then(item => {
 | 
				
			||||||
        return data;
 | 
					      if (!cacheItemExpired(item)) {
 | 
				
			||||||
 | 
					        return tryJSONparse(BG.LZString.decompressFromUTF16(item.payload));
 | 
				
			||||||
 | 
					      } else if (item) {
 | 
				
			||||||
 | 
					        chrome.storage.local.remove(key);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      BG.chromeLocal.remove(CACHE_PREFIX + id);
 | 
					 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  function writeCache(data) {
 | 
					  function writeCache(data, debounced) {
 | 
				
			||||||
    debounce(cleanupCache, 10e3);
 | 
					    if (!debounced) {
 | 
				
			||||||
    data.cacheWriteDate = Date.now();
 | 
					      debounce(writeCache, 100, data, true);
 | 
				
			||||||
    return BG.chromeLocal.setLZValue(CACHE_PREFIX + data.id, data)
 | 
					      return data;
 | 
				
			||||||
      .then(() => data);
 | 
					    } else {
 | 
				
			||||||
 | 
					      debounce(cleanupCache, CACHE_CLEANUP_THROTTLE);
 | 
				
			||||||
 | 
					      return BG.chromeLocal.setValue(CACHE_PREFIX + data.id, {
 | 
				
			||||||
 | 
					        payload: BG.LZString.compressToUTF16(JSON.stringify(data)),
 | 
				
			||||||
 | 
					        date: Date.now(),
 | 
				
			||||||
 | 
					      }).then(() => data);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function cacheItemExpired(item) {
 | 
				
			||||||
 | 
					    return !item || !item.date || Date.now() - item.date > CACHE_DURATION;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  function cleanupCache() {
 | 
					  function cleanupCache() {
 | 
				
			||||||
    new Promise(resolve =>
 | 
					    if (!chrome.storage.local.getBytesInUse) {
 | 
				
			||||||
      chrome.storage.local.getBytesInUse &&
 | 
					      chrome.storage.local.get(null, cleanupCacheInternal);
 | 
				
			||||||
      chrome.storage.local.getBytesInUse(null, resolve) ||
 | 
					    } else {
 | 
				
			||||||
      1e9
 | 
					      chrome.storage.local.getBytesInUse(null, size => {
 | 
				
			||||||
    )
 | 
					        if (size > CACHE_SIZE) {
 | 
				
			||||||
    .then(size => size > 1e6 || Promise.reject())
 | 
					          chrome.storage.local.get(null, cleanupCacheInternal);
 | 
				
			||||||
    .then(() => BG.chromeLocal.getValue(CACHE_PREFIX + 'Cleanup'))
 | 
					        }
 | 
				
			||||||
    .then((lastCleanup = 0) =>
 | 
					        ignoreChromeError();
 | 
				
			||||||
      Date.now() - lastCleanup > CACHE_DURATION &&
 | 
					      });
 | 
				
			||||||
      chrome.storage.local.get(null, storage => {
 | 
					 | 
				
			||||||
        const expired = [];
 | 
					 | 
				
			||||||
        for (const key in storage) {
 | 
					 | 
				
			||||||
          if (key.startsWith(CACHE_PREFIX) &&
 | 
					 | 
				
			||||||
              Date.now() - storage[key].cacheWriteDate > CACHE_DURATION) {
 | 
					 | 
				
			||||||
            expired.push(key);
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
        if (expired.length) {
 | 
					
 | 
				
			||||||
          chrome.storage.local.remove(expired);
 | 
					  function cleanupCacheInternal(storage) {
 | 
				
			||||||
 | 
					    const sortedByTime = Object.keys(storage)
 | 
				
			||||||
 | 
					      .filter(key => key.startsWith(CACHE_PREFIX))
 | 
				
			||||||
 | 
					      .map(key => Object.assign(storage[key], {key}))
 | 
				
			||||||
 | 
					      .sort((a, b) => a.date - b.date);
 | 
				
			||||||
 | 
					    const someExpired = cacheItemExpired(sortedByTime[0]);
 | 
				
			||||||
 | 
					    const expired = someExpired ? sortedByTime.filter(cacheItemExpired) :
 | 
				
			||||||
 | 
					      sortedByTime.slice(0, sortedByTime.length / 2);
 | 
				
			||||||
 | 
					    const toRemove = expired.length ? expired : sortedByTime;
 | 
				
			||||||
 | 
					    if (toRemove.length) {
 | 
				
			||||||
 | 
					      chrome.storage.local.remove(toRemove.map(item => item.key), ignoreChromeError);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
        BG.chromeLocal.setValue(CACHE_PREFIX + 'Cleanup', Date.now());
 | 
					    ignoreChromeError();
 | 
				
			||||||
      }))
 | 
					  }
 | 
				
			||||||
    .catch(ignoreChromeError);
 | 
					
 | 
				
			||||||
 | 
					  //endregion
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function objectPick(obj, keys) {
 | 
				
			||||||
 | 
					    const result = {};
 | 
				
			||||||
 | 
					    for (const k in obj) {
 | 
				
			||||||
 | 
					      if (keys.includes(k)) {
 | 
				
			||||||
 | 
					        result[k] = obj[k];
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    return result;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue
	
	Block a user