'use strict'; // Contains only auxiliary methods // May be included and executed unlimited number of times without any consequences // Polyfills for IE11 Array.prototype.find = Array.prototype.find || function (condition) { return this.filter(condition)[0]; }; Array.from = Array.from || function (source) { return Array.prototype.slice.call(source); }; NodeList.prototype.forEach = NodeList.prototype.forEach || function (callback) { Array.from(this).forEach(callback); }; String.prototype.includes = String.prototype.includes || function (searchString) { return this.indexOf(searchString) >= 0; }; String.prototype.startsWith = String.prototype.startsWith || function (prefix) { return this.substr(0, prefix.length) === prefix; }; Math.sign = Math.sign || function(x) { x = +x; if (!x) return x; // 0 and NaN return x > 0 ? 1 : -1; }; if (!window.hasOwnProperty('HTMLDetailsElement') && !window.hasOwnProperty('mockHTMLDetailsElement')) { window.mockHTMLDetailsElement = true; const style = 'details:not([open]) > :not(summary) {display: none}'; document.head.appendChild(document.createElement('style')).textContent = style; addEventListener('click', function (e) { if (e.target.nodeName !== 'SUMMARY') return; const details = e.target.parentElement; if (details.hasAttribute('open')) details.removeAttribute('open'); else details.setAttribute('open', ''); }); } // Monstrous global variable for handy code // Includes: clamp, xhr, storage.{get,set,remove} window.helpers = window.helpers || { /** * https://en.wikipedia.org/wiki/Clamping_(graphics) * @param {Number} num Source number * @param {Number} min Low border * @param {Number} max High border * @returns {Number} Clamped value */ clamp: function (num, min, max) { if (max < min) { var t = max; max = min; min = t; // swap max and min } if (max < num) return max; if (min > num) return min; return num; }, /** @private */ _xhr: function (method, url, options, callbacks) { const xhr = new XMLHttpRequest(); xhr.open(method, url); // Default options xhr.responseType = 'json'; xhr.timeout = 10000; // Default options redefining if (options.responseType) xhr.responseType = options.responseType; if (options.timeout) xhr.timeout = options.timeout; if (method === 'POST') xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); // better than onreadystatechange because of 404 codes https://stackoverflow.com/a/36182963 xhr.onloadend = function () { if (xhr.status === 200) { if (callbacks.on200) { // fix for IE11. It doesn't convert response to JSON if (xhr.responseType === '' && typeof(xhr.response) === 'string') callbacks.on200(JSON.parse(xhr.response)); else callbacks.on200(xhr.response); } } else { // handled by onerror if (xhr.status === 0) return; if (callbacks.onNon200) callbacks.onNon200(xhr); } }; xhr.ontimeout = function () { if (callbacks.onTimeout) callbacks.onTimeout(xhr); }; xhr.onerror = function () { if (callbacks.onError) callbacks.onError(xhr); }; if (options.payload) xhr.send(options.payload); else xhr.send(); }, /** @private */ _xhrRetry: function(method, url, options, callbacks) { if (options.retries <= 0) { console.warn('Failed to pull', options.entity_name); if (callbacks.onTotalFail) callbacks.onTotalFail(); return; } helpers._xhr(method, url, options, callbacks); }, /** * @callback callbackXhrOn200 * @param {Object} response - xhr.response */ /** * @callback callbackXhrError * @param {XMLHttpRequest} xhr */ /** * @param {'GET'|'POST'} method - 'GET' or 'POST' * @param {String} url - URL to send request to * @param {Object} options - other XHR options * @param {XMLHttpRequestBodyInit} [options.payload=null] - payload for POST-requests * @param {'arraybuffer'|'blob'|'document'|'json'|'text'} [options.responseType=json] * @param {Number} [options.timeout=10000] * @param {Number} [options.retries=1] * @param {String} [options.entity_name='unknown'] - string to log * @param {Number} [options.retry_timeout=1000] * @param {Object} callbacks - functions to execute on events fired * @param {callbackXhrOn200} [callbacks.on200] * @param {callbackXhrError} [callbacks.onNon200] * @param {callbackXhrError} [callbacks.onTimeout] * @param {callbackXhrError} [callbacks.onError] * @param {callbackXhrError} [callbacks.onTotalFail] - if failed after all retries */ xhr: function(method, url, options, callbacks) { if (!options.retries || options.retries <= 1) { helpers._xhr(method, url, options, callbacks); return; } if (!options.entity_name) options.entity_name = 'unknown'; if (!options.retry_timeout) options.retry_timeout = 1000; const retries_total = options.retries; let currentTry = 1; const retry = function () { console.warn('Pulling ' + options.entity_name + ' failed... ' + (currentTry++) + '/' + retries_total); setTimeout(function () { options.retries--; helpers._xhrRetry(method, url, options, callbacks); }, options.retry_timeout); }; // Pack retry() call into error handlers callbacks._onError = callbacks.onError; callbacks.onError = function (xhr) { if (callbacks._onError) callbacks._onError(xhr); retry(); }; callbacks._onTimeout = callbacks.onTimeout; callbacks.onTimeout = function (xhr) { if (callbacks._onTimeout) callbacks._onTimeout(xhr); retry(); }; helpers._xhrRetry(method, url, options, callbacks); }, /** * @typedef {Object} invidiousStorage * @property {(key:String) => Object} get * @property {(key:String, value:Object)} set * @property {(key:String)} remove */ /** * Universal storage, stores and returns JS objects. Uses inside localStorage or cookies * @type {invidiousStorage} */ storage: (function () { // access to localStorage throws exception in Tor Browser, so try is needed let localStorageIsUsable = false; try{localStorageIsUsable = !!localStorage.setItem;}catch(e){} if (localStorageIsUsable) { return { get: function (key) { if (!localStorage[key]) return; try { return JSON.parse(decodeURIComponent(localStorage[key])); } catch(e) { // Erase non parsable value helpers.storage.remove(key); } }, set: function (key, value) { localStorage[key] = encodeURIComponent(JSON.stringify(value)); }, remove: function (key) { localStorage.removeItem(key); } }; } // TODO: fire 'storage' event for cookies console.info('Storage: localStorage is disabled or unaccessible. Cookies used as fallback'); return { get: function (key) { const cookiePrefix = key + '='; function findCallback(cookie) {return cookie.startsWith(cookiePrefix);} const matchedCookie = document.cookie.split('; ').find(findCallback); if (matchedCookie) { const cookieBody = matchedCookie.replace(cookiePrefix, ''); if (cookieBody.length === 0) return; try { return JSON.parse(decodeURIComponent(cookieBody)); } catch(e) { // Erase non parsable value helpers.storage.remove(key); } } }, set: function (key, value) { const cookie_data = encodeURIComponent(JSON.stringify(value)); // Set expiration in 2 year const date = new Date(); date.setFullYear(date.getFullYear()+2); document.cookie = key + '=' + cookie_data + '; expires=' + date.toGMTString(); }, remove: function (key) { document.cookie = key + '=; Max-Age=0'; } }; })() };