Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
QuiteAFancyEmerald
GitHub Repository: QuiteAFancyEmerald/Holy-Unblocker
Path: blob/master/views/assets/js/common-1735118314.js
5247 views
1
/* -----------------------------------------------
2
/* Authors: QuiteAFancyEmerald, Yoct, and OlyB
3
/* GNU Affero General Public License v3.0: https://www.gnu.org/licenses/agpl-3.0.en.html
4
/* MAIN Holy Unblocker LTS Common Script
5
/* ----------------------------------------------- */
6
7
// Encase everything in a new scope so that variables are not accidentally
8
// attached to the global scope.
9
(() => {
10
11
/* GENERAL URL HANDLERS */
12
13
// To be defined after the document has fully loaded.
14
let uvConfig = {};
15
let sjEncode = {};
16
// Get the preferred apex domain name. Not exactly apex, as any
17
// subdomain other than those listed will be ignored.
18
const getDomain = () =>
19
location.host.replace(/^(?:www|edu|cooking|beta)\./, ''),
20
// This is used for stealth mode when visiting external sites.
21
goFrame = (url) => {
22
localStorage.setItem('{{hu-lts}}-frame-url', url);
23
if (location.pathname !== '{{route}}{{/s}}')
24
location.href = '{{route}}{{/s}}?cache={{cacheVal}}';
25
else document.getElementById('frame').src = url;
26
},
27
/* Used to set functions for the goProx object at the bottom.
28
* See the goProx object at the bottom for some usage examples
29
* on the URL handlers, omnibox functions, and the uvUrl and
30
* RammerheadEncode functions.
31
*/
32
urlHandler = (parser) =>
33
typeof parser === 'function'
34
? // Return different functions based on whether a URL has already been set.
35
// Should help avoid confusion when using or adding to the goProx object.
36
(url, mode) => {
37
if (!url) return;
38
url = parser(url);
39
mode = `${mode}`.toLowerCase();
40
if (mode === 'stealth' || mode == 1) goFrame(url);
41
else if (mode === 'window' || mode == 0) location.href = url;
42
else return url;
43
}
44
: (mode) => {
45
mode = `${mode}`.toLowerCase();
46
if (mode === 'stealth' || mode == 1) goFrame(parser);
47
else if (mode === 'window' || mode == 0) location.href = parser;
48
else return parser;
49
},
50
// An asynchronous version of the function above, just in case.
51
asyncUrlHandler = (parser) => async (url, mode) => {
52
if (!url) return;
53
if (typeof parser === 'function') url = await parser(url);
54
mode = `${mode}`.toLowerCase();
55
if (mode === 'stealth' || mode == 1) goFrame(url);
56
else if (mode === 'window' || mode == 0) location.href = url;
57
else return url;
58
};
59
60
/* READ SETTINGS */
61
62
const storageId = '{{hu-lts}}-storage',
63
storageObject = () => JSON.parse(localStorage.getItem(storageId)) || {},
64
readStorage = (name) => storageObject()[name];
65
66
/* OMNIBOX */
67
68
const searchEngines = Object.freeze({
69
'{{Startpage}}': 'startpage.com/sp/search?query=',
70
'{{Google}}': 'google.com/search?q=',
71
'{{Bing}}': 'bing.com/search?q=',
72
'{{DuckDuckGo}}': 'duckduckgo.com/?q=',
73
'{{Brave}}': 'search.brave.com/search?q=',
74
}),
75
defaultSearch = '{{defaultSearch}}',
76
autocompletes = Object.freeze({
77
// Startpage has used both Google's and Bing's autocomplete.
78
// For now, just use Bing.
79
'{{Startpage}}': 'www.bing.com/AS/Suggestions?csr=1&cvid=0&qry=',
80
'{{Google}}': 'www.google.com/complete/search?client=gws-wiz&callback=_&q=',
81
'{{Bing}}': 'www.bing.com/AS/Suggestions?csr=1&cvid=0&qry=',
82
'{{DuckDuckGo}}': 'duckduckgo.com/ac/?q=',
83
'{{Brave}}': 'search.brave.com/api/suggest?q=',
84
}),
85
autocompleteUrls = Object.values(autocompletes).map(
86
(url) => 'https://' + url
87
),
88
responseDelimiter = '\ue000',
89
formatSuggestion = (
90
suggestion,
91
delimiters,
92
newDelimiters = [responseDelimiter]
93
) => {
94
for (let i = 0; i < delimiters.length; i++)
95
suggestion = suggestion.replaceAll(
96
delimiters[i],
97
newDelimiters[i] || newDelimiters[0]
98
);
99
return suggestion;
100
},
101
responseHandlers = Object.freeze({
102
'{{Startpage}}': (jsonData) => responseHandlers['{{Bing}}'](jsonData),
103
'{{Google}}': (jsonData) =>
104
jsonData[0].map(([suggestion]) =>
105
formatSuggestion(suggestion, ['<b>', '</b>'])
106
),
107
'{{Bing}}': (jsonData) =>
108
jsonData.s.map(({ q }) => formatSuggestion(q, ['\ue000', '\ue001'])),
109
'{{DuckDuckGo}}': (jsonData) => jsonData.map(({ phrase }) => phrase),
110
'{{Brave}}': (jsonData) => jsonData[1],
111
});
112
113
// Get the autocomplete results for a given search query in JSON format.
114
const requestAC = async (
115
baseUrl,
116
query,
117
parserFunc = (url) => url,
118
params = {}
119
) => {
120
switch (parserFunc) {
121
case sjUrl: {
122
// Ask Scramjet to process the autocomplete request. Processed results will
123
// be returned to an event handler and are updated from there.
124
params.port.postMessage({
125
type: params.searchType,
126
request: {
127
url: parserFunc(baseUrl + encodeURIComponent(query)),
128
headers: new Map([['Date', params.time]]),
129
},
130
});
131
break;
132
}
133
case rhUrl: {
134
// Have Rammerhead process the autocomplete request.
135
const response = await fetch(
136
await parserFunc(baseUrl + encodeURIComponent(query))
137
),
138
responseType = response.headers.get('content-type');
139
let responseJSON = {};
140
if (responseType && responseType.indexOf('application/json') !== -1)
141
responseJSON = await response.json();
142
else
143
try {
144
responseJSON = await response.text();
145
try {
146
responseJSON = responseJSON.match(
147
/(?<=\/\*hammerhead\|.*header-end\*\/)[^]+?(?=\/\*hammerhead\|.*end\*\/)/i
148
)[0];
149
} catch (e) {
150
// In case Rammerhead chose not to encode the response.
151
}
152
try {
153
responseJSON = JSON.parse(responseJSON);
154
} catch (e) {
155
responseJSON = JSON.parse(
156
responseJSON.replace(/^[^[{]*|[^\]}]*$/g, '')
157
);
158
}
159
} catch (e) {
160
// responseJSON will be an empty object if everything was invalid.
161
}
162
163
// Update the autocomplete results directly.
164
updateAC(
165
params.prAC,
166
responseHandlers[params.searchType](responseJSON),
167
Date.parse(params.time)
168
);
169
break;
170
}
171
}
172
};
173
174
let lastUpdated = Date.parse(new Date().toUTCString());
175
const updateAC = (listElement, searchResults, time) => {
176
if (time < lastUpdated) return;
177
else lastUpdated = time;
178
// Update the data for the results.
179
listElement.textContent = '';
180
for (let i = 0; i < searchResults.length; i++) {
181
let suggestion = document.createElement('li');
182
suggestion.tabIndex = 0;
183
suggestion.append(
184
...searchResults[i].split(responseDelimiter).map((text, bolded) => {
185
if (bolded % 2) {
186
let node = document.createElement('b');
187
node.textContent = text;
188
return node;
189
}
190
return text;
191
})
192
);
193
listElement.appendChild(suggestion);
194
}
195
};
196
197
// Default search engine is set to Google. Intended to work just like the usual
198
// bar at the top of a browser.
199
const getSearchTemplate = (
200
searchEngine = searchEngines[readStorage('SearchEngine')] ||
201
searchEngines[defaultSearch]
202
) => `https://${searchEngine}%s`,
203
// Like an omnibox, return the results of a search engine if search terms are
204
// provided instead of a URL.
205
search = (input) => {
206
try {
207
// Return the input if it is already a valid URL.
208
// eg: https://example.com, https://example.com/test?q=param
209
return new URL(input) + '';
210
} catch (e) {
211
// Continue if it is invalid.
212
}
213
214
try {
215
// Check if the input is valid when http:// is added to the start.
216
// eg: example.com, https://example.com/test?q=param
217
const url = new URL(`http://${input}`);
218
// Return only if the hostname has a TLD or a subdomain.
219
if (url.hostname.indexOf('.') != -1) return url + '';
220
} catch (e) {
221
// Continue if it is invalid.
222
}
223
224
// Treat the input as a search query instead of a website.
225
return getSearchTemplate().replace('%s', encodeURIComponent(input));
226
},
227
// Parse a URL to use with Ultraviolet.
228
uvUrl = (url) => {
229
try {
230
url = location.origin + uvConfig.prefix + uvConfig.encodeUrl(search(url));
231
} catch (e) {
232
// This is for cases where the Ultraviolet scripts have not been loaded.
233
url = search(url);
234
}
235
return url;
236
},
237
// Parse a URL to use with Scramjet.
238
sjUrl = (url) => {
239
try {
240
url = location.origin + sjEncode(search(url));
241
} catch (e) {
242
// This is for cases where the SJ scripts have not been loaded.
243
url = search(url);
244
}
245
return url;
246
},
247
rhUrl = async (url) =>
248
location.origin + (await RammerheadEncode(search(url)));
249
250
/* RAMMERHEAD CONFIGURATION */
251
252
// Store the search autocomplete string shuffler until reloaded.
253
// The ID must be a string containing 32 alphanumerical characters.
254
let rhACDict = { id: 'collectsearchautocompleteresults', dict: '' };
255
256
// Parse a URL to use with Rammerhead. Only usable if the server is active.
257
const RammerheadEncode = async (baseUrl) => {
258
// Hellhead
259
const mod = (n, m) => ((n % m) + m) % m,
260
baseDictionary =
261
'0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz~-',
262
shuffledIndicator = '_rhs',
263
// Return a copy of the base dictionary with a randomized character order.
264
// Will be used as a Caesar cipher for URL encoding.
265
generateDictionary = () => {
266
let str = '';
267
const split = baseDictionary.split('');
268
while (split.length > 0) {
269
// Using .splice automatically rounds down to the nearest whole number.
270
str += split.splice(Math.random() * split.length, 1)[0];
271
}
272
return str;
273
};
274
275
class StrShuffler {
276
constructor(dictionary = generateDictionary()) {
277
this.dictionary = dictionary;
278
}
279
280
shuffle(str) {
281
// Do not reshuffle an already shuffled string.
282
if (!str.indexOf(shuffledIndicator)) return str;
283
284
let shuffledStr = '';
285
for (let i = 0; i < str.length; i++) {
286
const char = str[i],
287
idx = baseDictionary.indexOf(char);
288
289
/* For URL encoded characters and characters not included in the
290
* dictionary, leave untouched. Otherwise, replace with a character
291
* from the dictionary.
292
*/
293
if (char === '%' && str.length - i >= 3)
294
// A % symbol denotes that the next 2 characters are URL encoded.
295
shuffledStr += char + str[++i] + str[++i];
296
// Do not modify unrecognized characters.
297
else if (idx == -1) shuffledStr += char;
298
// Find the corresponding dictionary entry and use the character
299
// that is i places to the right of it.
300
else
301
shuffledStr += this.dictionary[mod(idx + i, baseDictionary.length)];
302
}
303
// Add a prefix signifying that the string has been shuffled.
304
return shuffledIndicator + shuffledStr;
305
}
306
307
// Unshuffling is currently not done on the client side, and likely
308
// won't ever be for this implementation. It is used by the server instead.
309
unshuffle(str) {
310
// Do not unshuffle an already unshuffled string.
311
if (str.indexOf(shuffledIndicator)) return str;
312
313
// Remove the prefix signifying that the string has been shuffled.
314
str = str.slice(shuffledIndicator.length);
315
316
let unshuffledStr = '';
317
for (let i = 0; i < str.length; i++) {
318
const char = str[i],
319
idx = this.dictionary.indexOf(char);
320
321
/* Convert the dictionary entry characters back into their base
322
* characters using the base dictionary. Again, leave URL encoded
323
* characters and unrecognized symbols alone.
324
*/
325
if (char === '%' && str.length - i >= 3)
326
unshuffledStr += char + str[++i] + str[++i];
327
else if (idx == -1) unshuffledStr += char;
328
// Find the corresponding base character entry and use the character
329
// that is i places to the left of it.
330
else
331
unshuffledStr += baseDictionary[mod(idx - i, baseDictionary.length)];
332
}
333
return unshuffledStr;
334
}
335
}
336
337
// Request information that's beiing stored elsewhere on the server.
338
// Executes the callback function if the server responds as intended.
339
const get = (url, callback, shush = false) => {
340
let request = new XMLHttpRequest();
341
request.open('GET', url, true);
342
request.send();
343
344
request.onerror = () => {
345
if (!shush) console.log('Cannot communicate with the server');
346
};
347
request.onload = () => {
348
if (request.status === 200) callback(request.responseText);
349
else if (!shush)
350
console.log(
351
`Unexpected server response to not match "200". Server says "${request.responseText}"`
352
);
353
};
354
},
355
// Functions for interacting with Rammerhead backend code on the server.
356
api = {
357
// Make a new Rammerhead session and do something with it.
358
newsession(callback) {
359
get('{{route}}{{/newsession}}', callback);
360
},
361
362
// Check if a session with the specified ID exists, then do something.
363
sessionexists(id, callback) {
364
get(
365
'{{route}}{{/sessionexists}}?id=' + encodeURIComponent(id),
366
(res) => {
367
if (res === 'exists') return callback(true);
368
if (res === 'not found') return callback(false);
369
console.log('Unexpected response from server. Received ' + res);
370
}
371
);
372
},
373
374
// Request a brand new encoding table to use for Rammerhead.
375
shuffleDict(id, callback) {
376
console.log('Shuffling', id);
377
get(
378
'{{route}}{{/api/shuffleDict}}?id=' + encodeURIComponent(id),
379
(res) => {
380
callback(JSON.parse(res));
381
}
382
);
383
},
384
},
385
/* Organize Rammerhead sessions via the browser's local storage.
386
* Local data consists of session creation timestamps and session IDs.
387
* The rest of the data is stored on the server.
388
*/
389
localStorageKey = 'rammerhead_sessionids',
390
localStorageKeyDefault = 'rammerhead_default_sessionid',
391
sessionIdsStore = {
392
// Get the local data of all stored sessions.
393
get() {
394
const rawData = localStorage.getItem(localStorageKey);
395
if (!rawData) return [];
396
try {
397
const data = JSON.parse(rawData);
398
399
// Catch invalidly stored Rammerhead session data. Either that or
400
// it's poorly spoofed.
401
if (!Array.isArray(data)) throw 'getout';
402
return data;
403
} catch (e) {
404
return [];
405
}
406
},
407
408
// Store local Rammerhead session data in the form of an array.
409
set(data) {
410
if (!Array.isArray(data)) throw new TypeError('Must be an array.');
411
localStorage.setItem(localStorageKey, JSON.stringify(data));
412
},
413
414
// Get the default session data.
415
getDefault() {
416
const sessionId = localStorage.getItem(localStorageKeyDefault);
417
if (sessionId) {
418
let data = sessionIdsStore.get();
419
data.filter((session) => session.id === sessionId);
420
if (data.length) return data[0];
421
}
422
return null;
423
},
424
425
// Set a new default session based on a given session ID.
426
setDefault(id) {
427
localStorage.setItem(localStorageKeyDefault, id);
428
},
429
},
430
// Store or update local data for a Rammerhead session, which consists of
431
// the session's ID and when the session was last created.
432
addSession = (id) => {
433
let data = sessionIdsStore.get();
434
data.unshift({ id: id, createdOn: new Date().toLocaleString() });
435
sessionIdsStore.set(data);
436
},
437
// Attempt to load an existing session that has been stored on the server.
438
getSessionId = (baseUrl) => {
439
return new Promise((resolve) => {
440
for (let i = 0; i < autocompleteUrls.length; i++)
441
if (baseUrl.indexOf(autocompleteUrls[i]) === 0)
442
return resolve(rhACDict.id);
443
// Check if the browser has stored an existing session.
444
const id = localStorage.getItem('session-string');
445
api.sessionexists(id, (value) => {
446
// Create a new session if Rammerhead can't find an existing session.
447
if (!value) {
448
console.log('Session validation failed');
449
api.newsession((id) => {
450
addSession(id);
451
localStorage.setItem('session-string', id);
452
console.log(id);
453
console.log('^ new id');
454
resolve(id);
455
});
456
}
457
// Load the stored session now that Rammerhead has found it.
458
else resolve(id);
459
});
460
});
461
};
462
463
// Load the URL that was last visited in the Rammerhead session.
464
return getSessionId(baseUrl).then((id) => {
465
if (id === rhACDict.id && rhACDict.dict)
466
return new Promise((resolve) => {
467
resolve(
468
`{{route}}{{/}}${id}/` +
469
new StrShuffler(rhACDict.dict).shuffle(baseUrl)
470
);
471
});
472
return new Promise((resolve) => {
473
api.shuffleDict(id, (shuffleDict) => {
474
if (id === rhACDict.id) rhACDict.dict = shuffleDict;
475
// Encode the URL with Rammerhead's encoding table and return the URL.
476
resolve(
477
`{{route}}{{/}}${id}/` + new StrShuffler(shuffleDict).shuffle(baseUrl)
478
);
479
});
480
});
481
});
482
};
483
484
/* To use:
485
* goProx.proxy(url-string, mode-as-string-or-number);
486
*
487
* Key: 1 = "stealth"
488
* 0 = "window"
489
* Nothing = return URL as a string
490
*
491
* Examples:
492
* Stealth mode -
493
* goProx.ultraviolet("https://google.com", 1);
494
* goProx.ultraviolet("https://google.com", "stealth");
495
*
496
* await goProx.rammerhead("https://google.com", 1);
497
* await goProx.rammerhead("https://google.com", "stealth");
498
*
499
* goProx.searx(1);
500
* goProx.searx("stealth");
501
*
502
* Window mode -
503
* goProx.ultraviolet("https://google.com", "window");
504
*
505
* await goProx.rammerhead("https://google.com", "window");
506
*
507
* goProx.searx("window");
508
*
509
* Return string value mode (default) -
510
* goProx.ultraviolet("https://google.com");
511
*
512
* await goProx.rammerhead("https://google.com");
513
*
514
* goProx.searx();
515
*/
516
const preparePage = async () => {
517
// This won't break the service workers as they store the variable separately.
518
uvConfig = self['{{__uv$config}}'];
519
sjObject = self['$scramjetLoadController'];
520
if (sjObject)
521
sjEncode = new (sjObject().ScramjetController)({
522
prefix: '{{route}}{{/scram/network/}}',
523
}).encodeUrl;
524
525
// Object.freeze prevents goProx from accidentally being edited.
526
const goProx = Object.freeze({
527
// `location.protocol + "//" + getDomain()` more like `location.origin`
528
// setAuthCookie("__cor_auth=1", false);
529
ultraviolet: urlHandler(uvUrl),
530
531
scramjet: urlHandler(sjUrl),
532
533
rammerhead: asyncUrlHandler(rhUrl),
534
535
searx: urlHandler(location.protocol + `//c.${getDomain()}/engine/`),
536
537
libreddit: urlHandler(location.protocol + '//c.' + getDomain()),
538
539
rnav: urlHandler(location.protocol + '//client.' + getDomain()),
540
541
osu: urlHandler(location.origin + '{{route}}{{/archive/osu}}'),
542
543
agar: urlHandler(sjUrl('https://agar.io')),
544
545
tru: urlHandler(sjUrl('https://truffled.lol/g')),
546
547
prison: urlHandler(sjUrl('https://vimlark.itch.io/pick-up-prison')),
548
549
speed: urlHandler(sjUrl('https://captain4lk.itch.io/what-the-road-brings')),
550
551
heli: urlHandler(sjUrl('https://benjames171.itch.io/helo-storm')),
552
553
youtube: urlHandler(sjUrl('https://youtube.com')),
554
555
invidious: urlHandler(sjUrl('https://invidious.snopyta.org')),
556
557
chatgpt: urlHandler(sjUrl('https://chat.openai.com/chat')),
558
559
discord: urlHandler(sjUrl('https://discord.com/app')),
560
561
geforcenow: urlHandler(sjUrl('https://play.geforcenow.com/mall')),
562
563
spotify: urlHandler(sjUrl('https://open.spotify.com')),
564
565
tiktok: urlHandler(sjUrl('https://www.tiktok.com')),
566
567
hianime: urlHandler(sjUrl('https://www.hianime.to')),
568
569
twitter: urlHandler(sjUrl('https://twitter.com')),
570
571
twitch: urlHandler(sjUrl('https://www.twitch.tv')),
572
573
instagram: urlHandler(sjUrl('https://www.instagram.com')),
574
575
reddit: urlHandler(sjUrl('https://www.reddit.com')),
576
577
wikipedia: urlHandler(sjUrl('https://www.wikiwand.com')),
578
579
newgrounds: urlHandler(sjUrl('https://www.newgrounds.com')),
580
});
581
582
// Call a function after a given number of service workers are active.
583
// Workers are appended as additional arguments to the callback.
584
const callAfterWorkers = async (
585
urls,
586
callback,
587
afterHowMany = 1,
588
tries = 10,
589
...params
590
) => {
591
// For 10 tries, stop after 10 seconds of no response from workers.
592
if (tries <= 0) return console.log('Failed to recognize service workers.');
593
const workers = await Promise.all(
594
urls.map((url) => navigator.serviceWorker.getRegistration(url))
595
);
596
let newUrls = [],
597
finishedWorkers = [];
598
for (let i = 0; i < workers.length; i++) {
599
if (workers[i] && workers[i].active) {
600
afterHowMany--;
601
finishedWorkers.push(workers[i]);
602
} else newUrls.push(urls[i]);
603
}
604
if (afterHowMany <= 0) return await callback(...params, ...finishedWorkers);
605
else
606
await Promise.race([
607
navigator.serviceWorker.ready,
608
new Promise((resolve) => {
609
setTimeout(() => {
610
tries--;
611
resolve();
612
}, 1000);
613
}),
614
]);
615
return await callAfterWorkers(
616
newUrls,
617
callback,
618
afterHowMany,
619
tries,
620
...params,
621
...finishedWorkers
622
);
623
};
624
625
// Attach event listeners using goProx to specific app menus that need it.
626
const prSet = (id, type) => {
627
const formElement = document.getElementById(id);
628
if (!formElement) return;
629
630
let prUrl = formElement.querySelector('input[type=text]'),
631
prAC = formElement.querySelector('#autocomplete'),
632
prGo1 = document.querySelectorAll(`#${id}.pr-go1, #${id} .pr-go1`),
633
prGo2 = document.querySelectorAll(`#${id}.pr-go2, #${id} .pr-go2`);
634
635
// Handle the other menu buttons differently if there is no omnibox. Menus
636
// which lack an omnibox likely use buttons as mere links.
637
const goProxMethod = prUrl
638
? (mode) => () => {
639
goProx[type](prUrl.value, mode);
640
}
641
: (mode) => () => {
642
goProx[type](mode);
643
},
644
// Ultraviolet and Scramjet are currently incompatible with window mode.
645
defaultModes = {
646
globalDefault: 'window',
647
ultraviolet: 'stealth',
648
scramjet: 'stealth',
649
rammerhead: 'window',
650
},
651
searchMode = defaultModes[type] || defaultModes['globalDefault'];
652
653
if (prUrl) {
654
let enableSearch = false,
655
onCooldown = false;
656
657
prUrl.addEventListener('keydown', async (e) => {
658
if (e.code === 'Enter') goProxMethod(searchMode)();
659
// This is exclusively used for the validator script.
660
else if (e.code === 'Validator Test') {
661
e.target.value = await goProx[type](e.target.value);
662
e.target.dispatchEvent(new Event('change'));
663
}
664
});
665
666
if (prAC) {
667
// Set up a message channel to communicate with Scramjet, if it exists.
668
let autocompleteChannel = {},
669
sjLoaded = false;
670
if (sjObject) {
671
autocompleteChannel = new MessageChannel();
672
callAfterWorkers(['{{route}}{{/scram/scramjet.sw.js}}'], (worker) => {
673
worker.active.postMessage({ type: 'requestAC' }, [
674
autocompleteChannel.port2,
675
]);
676
sjLoaded = true;
677
});
678
679
// Update the autocomplete results if Scramjet has processed them.
680
autocompleteChannel.port1.addEventListener('message', ({ data }) => {
681
updateAC(
682
prAC,
683
responseHandlers[data.searchType](data.responseJSON),
684
Date.parse(data.time)
685
);
686
sjLoaded = true;
687
});
688
689
autocompleteChannel.port1.start();
690
}
691
692
// Get autocomplete search results when typing in the omnibox.
693
prUrl.addEventListener('input', async (e) => {
694
// Prevent excessive fetch requests by restricting when requests are made.
695
if (enableSearch && !onCooldown) {
696
if (!e.target.value) {
697
prAC.textContent = '';
698
return;
699
}
700
const query = e.target.value;
701
if (e.isTrusted) {
702
onCooldown = true;
703
setTimeout(() => {
704
onCooldown = false;
705
// Refresh the autocomplete results after the cooldown ends.
706
if (query !== e.target.value)
707
e.target.dispatchEvent(new Event('input'));
708
}, 600);
709
}
710
711
// Get autocomplete results from the selected search engine.
712
let searchType = readStorage('SearchEngine');
713
if (!(searchType in autocompletes)) searchType = defaultSearch;
714
const requestTime = new Date().toUTCString();
715
if (sjLoaded) {
716
sjLoaded = false;
717
requestAC('https://' + autocompletes[searchType], query, sjUrl, {
718
searchType: searchType,
719
port: autocompleteChannel.port1,
720
time: requestTime,
721
});
722
} else
723
requestAC('https://' + autocompletes[searchType], query, rhUrl, {
724
searchType: searchType,
725
prAC: prAC,
726
time: requestTime,
727
});
728
}
729
});
730
731
// Show autocomplete results only if the omnibox is in focus.
732
prUrl.addEventListener('focus', () => {
733
// Don't show results if they were disabled by the user.
734
if (readStorage('UseAC') !== false) {
735
enableSearch = true;
736
prAC.classList.toggle('display-off', false);
737
}
738
prUrl.select();
739
});
740
prUrl.addEventListener('blur', (e) => {
741
enableSearch = false;
742
743
// Do not remove the autocomplete result list if it was being clicked.
744
if (e.relatedTarget) {
745
e.relatedTarget.focus();
746
if (document.activeElement.parentNode === prAC) return;
747
}
748
749
prAC.classList.toggle('display-off', true);
750
});
751
752
// Make the corresponding search query if a given suggestion was clicked.
753
prAC.addEventListener('click', (e) => {
754
e.target.focus();
755
prUrl.value = document.activeElement.textContent;
756
goProxMethod(searchMode)();
757
});
758
}
759
}
760
761
prGo1.forEach((element) => {
762
element.addEventListener('click', goProxMethod('window'));
763
});
764
prGo2.forEach((element) => {
765
element.addEventListener('click', goProxMethod('stealth'));
766
});
767
};
768
769
prSet('pr-uv', 'ultraviolet');
770
prSet('pr-sj', 'scramjet');
771
prSet('pr-rh', 'rammerhead');
772
prSet('pr-yt', 'youtube');
773
prSet('pr-iv', 'invidious');
774
prSet('pr-cg', 'chatgpt');
775
prSet('pr-dc', 'discord');
776
prSet('pr-gf', 'geforcenow');
777
prSet('pr-sp', 'spotify');
778
prSet('pr-tt', 'tiktok');
779
prSet('pr-ha', 'hianime');
780
prSet('pr-tw', 'twitter');
781
prSet('pr-tc', 'twitch');
782
prSet('pr-ig', 'instagram');
783
prSet('pr-rt', 'reddit');
784
prSet('pr-wa', 'wikipedia');
785
prSet('pr-ng', 'newgrounds');
786
787
// Load the frame for stealth mode if it exists.
788
const windowFrame = document.getElementById('frame'),
789
loadFrame = () => {
790
windowFrame.src = localStorage.getItem('{{hu-lts}}-frame-url');
791
return true;
792
};
793
if (windowFrame) {
794
if (uvConfig && sjObject)
795
(await callAfterWorkers(
796
[
797
'{{route}}{{/scram/scramjet.sw.js}}',
798
'{{route}}{{/uv/sw.js}}',
799
'{{route}}{{/uv/sw-blacklist.js}}',
800
],
801
loadFrame,
802
2,
803
3
804
)) || loadFrame();
805
else loadFrame();
806
}
807
808
const useModule = (moduleFunc, tries = 0) => {
809
try {
810
moduleFunc();
811
} catch (e) {
812
if (tries <= 5)
813
setTimeout(() => {
814
useModule(moduleFunc, tries + 1);
815
}, 600);
816
}
817
};
818
819
if (document.getElementsByClassName('tippy-button').length >= 0)
820
useModule(() => {
821
tippy('.tippy-button', {
822
delay: 50,
823
animateFill: true,
824
placement: 'bottom',
825
});
826
});
827
if (document.getElementsByClassName('pr-tippy').length >= 0)
828
useModule(() => {
829
tippy('.pr-tippy', {
830
delay: 50,
831
animateFill: true,
832
placement: 'bottom',
833
});
834
});
835
836
const banner = document.getElementById('banner');
837
if (banner) {
838
useModule(() => {
839
AOS.init();
840
});
841
842
fetch('{{route}}{{/assets/json/splash.json}}', {
843
mode: 'same-origin',
844
}).then((response) => {
845
response.json().then((splashList) => {
846
banner.firstElementChild.innerHTML =
847
splashList[(Math.random() * splashList.length) | 0];
848
});
849
});
850
}
851
852
// Load in relevant JSON files used to organize large sets of data.
853
// This first one is for links, whereas the rest are for navigation menus.
854
fetch('{{route}}{{/assets/json/links.json}}', {
855
mode: 'same-origin',
856
}).then((response) => {
857
response.json().then((huLinks) => {
858
for (let items = Object.entries(huLinks), i = 0; i < items.length; i++)
859
// Replace all placeholder links with the corresponding entry in huLinks.
860
(document.getElementById(items[i][0]) || {}).href = items[i][1];
861
});
862
});
863
864
const navLists = {
865
// Pair an element ID with a JSON file name. They are identical for now.
866
'emu-nav': 'emu-nav',
867
'emulib-nav': 'emulib-nav',
868
'flash-nav': 'flash-nav',
869
'h5-nav': 'h5-nav',
870
'par-nav': 'par-nav',
871
};
872
873
for (const [listId, filename] of Object.entries(navLists)) {
874
let navList = document.getElementById(listId);
875
876
if (navList) {
877
// List items stored in JSON format will be returned as a JS object.
878
const data = await fetch(`{{route}}{{/assets/json/}}${filename}.json`, {
879
mode: 'same-origin',
880
}).then((response) => response.json());
881
882
// Load the JSON lists into specific HTML parent elements as groups of
883
// child elements, if the parent element is found.
884
switch (filename) {
885
case 'emu-nav':
886
case 'emulib-nav':
887
case 'par-nav':
888
case 'h5-nav': {
889
const dirnames = {
890
// Set the directory of where each item of the corresponding JSON
891
// list will be retrieved from.
892
'emu-nav': 'emu',
893
'emulib-nav': 'emulib',
894
'par-nav': 'par',
895
'h5-nav': 'h5g',
896
},
897
dir = dirnames[filename],
898
// Add a little functionality for each list item when clicked on.
899
clickHandler = (parser, a) => (e) => {
900
if (e.target == a || e.target.tagName != 'A') {
901
e.preventDefault();
902
parser();
903
}
904
};
905
906
for (let i = 0; i < data.length; i++) {
907
// Load each item as an anchor tag with an image, heading,
908
// and click event listener.
909
const item = data[i],
910
a = document.createElement('a'),
911
img = document.createElement('img'),
912
title = document.createElement('h3');
913
((desc = document.createElement('p')),
914
(credits = document.createElement('p')));
915
916
a.href = '#';
917
img.src = `{{route}}{{/assets/img/}}${dir}/` + item.img;
918
title.textContent = item.name;
919
desc.textContent = item.description;
920
credits.textContent = item.credits;
921
922
if (filename === 'par-nav') {
923
if (item.credits === 'truf')
924
desc.innerHTML +=
925
'<br>{{mask}}{{Credits: Check out the full site at }}<a target="_blank" href="{{route}}{{/truffled}}">{{mask}}{{truffled.lol}}</a> //{{mask}}{{ discord.gg/vVqY36mzvj}}';
926
}
927
928
a.appendChild(img);
929
a.appendChild(title);
930
a.appendChild(desc);
931
932
// Which function is used for the click event is determined by
933
// the corresponding location/index in the dirnames object.
934
const functionsList = [
935
// emu-nav
936
() => goFrame(item.path),
937
// emulib-nav
938
() =>
939
goFrame(
940
'{{route}}{{/webretro}}?core=' +
941
item.core +
942
'&rom=' +
943
item.rom
944
),
945
// par-nav
946
item.custom && goProx[item.custom]
947
? () => goProx[item.custom]('stealth')
948
: () => {},
949
// h5-nav
950
item.custom && goProx[item.custom]
951
? () => goProx[item.custom]('stealth')
952
: () => goFrame('{{route}}{{/archive/g/}}' + item.path),
953
];
954
955
a.addEventListener(
956
'click',
957
clickHandler(
958
functionsList[Object.values(dirnames).indexOf(dir)],
959
a
960
)
961
);
962
963
navList.appendChild(a);
964
}
965
break;
966
}
967
968
case 'flash-nav':
969
for (let i = 0; i < data.length; i++) {
970
// Load each item as an anchor tag with a short title and click
971
// event listener.
972
const item = data[i],
973
a = document.createElement('a');
974
a.href = '#';
975
a.textContent = item.slice(0, -4);
976
977
a.addEventListener('click', (e) => {
978
e.preventDefault();
979
goFrame('{{route}}{{/flash}}?swf=' + item);
980
});
981
982
navList.appendChild(a);
983
}
984
break;
985
986
// No default case.
987
}
988
}
989
}
990
};
991
if ('loading' === document.readyState)
992
addEventListener('DOMContentLoaded', preparePage);
993
else preparePage();
994
})();
995
996