File size: 9,282 Bytes
ad9076c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
'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) {
                    let storageItem = localStorage.getItem(key)
                    if (!storageItem) return;
                    try {
                        return JSON.parse(decodeURIComponent(storageItem));
                    } catch(e) {
                        // Erase non parsable value
                        helpers.storage.remove(key);
                    }
                },
                set: function (key, value) { 
                    let encoded_value = encodeURIComponent(JSON.stringify(value))
                    localStorage.setItem(key, encoded_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';
            }
        };
    })()
};