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