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