Path: blob/master/views/assets/js/common-1778310233.js
14684 views
/* -----------------------------------------------1/* Authors: QuiteAFancyEmerald, Yoct, b4kt, and OlyB2/* GNU Affero General Public License v3.0: https://www.gnu.org/licenses/agpl-3.0.en.html3/* MAIN InvisiProxy LTS Common Script4/* ----------------------------------------------- */56// Encase everything in a new scope so that variables are not accidentally7// attached to the global scope.8(() => {910/* GENERAL URL HANDLERS */1112// To be defined after the document has fully loaded.13let uvConfig = {};14let sjEncode = {};15// Get the preferred apex domain name. Not exactly apex, as any16// subdomain other than those listed will be ignored.17const getDomain = () =>18location.host.replace(/^(?:www|beta)\./, ''),19// This is used for stealth mode when visiting external sites.20goFrame = (url) => {21localStorage.setItem('{{hu-lts}}-frame-url', url);22if (location.pathname !== '{{route}}{{/s}}')23location.href = '{{route}}{{/s}}?cache={{cacheVal}}';24else document.getElementById('frame').src = url;25},26/* Used to set functions for the goProx object at the bottom.27* See the goProx object at the bottom for some usage examples28* on the URL handlers, omnibox functions, and the uvUrl and29* RammerheadEncode functions.30*/31urlHandler = (parser) =>32typeof parser === 'function'33? // Return different functions based on whether a URL has already been set.34// Should help avoid confusion when using or adding to the goProx object.35(url, mode) => {36if (!url) return;37url = parser(url);38mode = `${mode}`.toLowerCase();39if (mode === 'stealth' || mode == 1) goFrame(url);40else if (mode === 'window' || mode == 0) location.href = url;41else return url;42}43: (mode) => {44mode = `${mode}`.toLowerCase();45if (mode === 'stealth' || mode == 1) goFrame(parser);46else if (mode === 'window' || mode == 0) location.href = parser;47else return parser;48},49openBlankCloak = () => {50try {51const newWindow = window.open('about:blank', '_blank');52if (!newWindow) return null;53const iframe = newWindow.document.createElement('iframe');54const styles = {55border: 'none',56width: '100%',57height: '100%',58margin: '0',59overflow: 'hidden',60};61Object.assign(iframe.style, styles);62iframe.src = location.href;63newWindow.document.body.appendChild(iframe);64return newWindow;65} catch (e) {66console.error('Blank cloaking failed:', e);67return null;68}69},70openBlobCloak = () => {71try {72const icon =73(document.querySelector("link[rel*='icon']") || {}).href || '';74const html = `<!DOCTYPE html><html><head><title>${75document.title76}</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="${77location.href78}"></iframe></body></html>`;79const blob = new Blob([html], { type: 'text/html' });80const blobUrl = URL.createObjectURL(blob);81const newWindow = window.open(blobUrl, '_blank');82return newWindow;83} catch (e) {84console.error('Blob cloaking failed:', e);85return null;86}87},88// An asynchronous version of the function above, just in case.89asyncUrlHandler = (parser) => async (url, mode) => {90if (!url) return;91if (typeof parser === 'function') url = await parser(url);92mode = `${mode}`.toLowerCase();93if (mode === 'stealth' || mode == 1) goFrame(url);94else if (mode === 'window' || mode == 0) location.href = url;95else return url;96};9798/* READ SETTINGS */99100const storageId = '{{hu-lts}}-storage',101storageObject = () => JSON.parse(localStorage.getItem(storageId)) || {},102readStorage = (name) => storageObject()[name];103104/* OMNIBOX */105106const searchEngines = Object.freeze({107'{{Startpage}}': 'startpage.com/sp/search?query=',108// '{{Google}}': 'google.com/search?q=',109'{{Bing}}': 'bing.com/search?q=',110'{{DuckDuckGo}}': 'duckduckgo.com/?q=',111'{{Brave}}': 'search.brave.com/search?q=',112}),113defaultSearch = '{{defaultSearch}}',114autocompletes = Object.freeze({115// Startpage has used both Google's and Bing's autocomplete.116// For now, just use Bing.117'{{Startpage}}': 'www.bing.com/AS/Suggestions?csr=1&cvid=0&qry=',118// '{{Google}}': 'www.google.com/complete/search?client=gws-wiz&callback=_&q=',119'{{Bing}}': 'www.bing.com/AS/Suggestions?csr=1&cvid=0&qry=',120'{{DuckDuckGo}}': 'duckduckgo.com/ac/?q=',121'{{Brave}}': 'search.brave.com/api/suggest?q=',122}),123autocompleteUrls = Object.values(autocompletes).map(124(url) => 'https://' + url125),126responseDelimiter = '\ue000',127formatSuggestion = (128suggestion,129delimiters,130newDelimiters = [responseDelimiter]131) => {132for (let i = 0; i < delimiters.length; i++)133suggestion = suggestion.replaceAll(134delimiters[i],135newDelimiters[i] || newDelimiters[0]136);137return suggestion;138},139responseHandlers = Object.freeze({140'{{Startpage}}': (jsonData) => responseHandlers['{{Bing}}'](jsonData),141/* '{{Google}}': (jsonData) =>142jsonData[0].map(([suggestion]) =>143formatSuggestion(suggestion, ['<b>', '</b>'])144),145*/146'{{Bing}}': (jsonData) =>147jsonData.s.map(({ q }) => formatSuggestion(q, ['\ue000', '\ue001'])),148'{{DuckDuckGo}}': (jsonData) => jsonData.map(({ phrase }) => phrase),149'{{Brave}}': (jsonData) => jsonData[1],150});151152// Get the autocomplete results for a given search query in JSON format.153const requestAC = async (154baseUrl,155query,156parserFunc = (url) => url,157params = {}158) => {159switch (parserFunc) {160case sjUrl: {161// Ask Scramjet to process the autocomplete request. Processed results will162// be returned to an event handler and are updated from there.163params.port.postMessage({164type: params.searchType,165request: {166url: parserFunc(baseUrl + encodeURIComponent(query)),167headers: new Map([['Date', params.time]]),168},169});170break;171}172case rhUrl: {173// Have Rammerhead process the autocomplete request.174const response = await fetch(175await parserFunc(baseUrl + encodeURIComponent(query))176),177responseType = response.headers.get('content-type');178let responseJSON = {};179if (responseType && responseType.indexOf('application/json') !== -1)180responseJSON = await response.json();181else182try {183responseJSON = await response.text();184try {185responseJSON = responseJSON.match(186/(?<=\/\*hammerhead\|.*header-end\*\/)[^]+?(?=\/\*hammerhead\|.*end\*\/)/i187)[0];188} catch (e) {189// In case Rammerhead chose not to encode the response.190}191try {192responseJSON = JSON.parse(responseJSON);193} catch (e) {194responseJSON = JSON.parse(195responseJSON.replace(/^[^[{]*|[^\]}]*$/g, '')196);197}198} catch (e) {199// responseJSON will be an empty object if everything was invalid.200}201202// Update the autocomplete results directly.203updateAC(204params.prAC,205responseHandlers[params.searchType](responseJSON),206Date.parse(params.time)207);208break;209}210}211};212213let lastUpdated = Date.parse(new Date().toUTCString());214const updateAC = (listElement, searchResults, time) => {215if (time < lastUpdated) return;216else lastUpdated = time;217// Update the data for the results.218listElement.textContent = '';219for (let i = 0; i < searchResults.length; i++) {220let suggestion = document.createElement('li');221suggestion.tabIndex = 0;222suggestion.append(223...searchResults[i].split(responseDelimiter).map((text, bolded) => {224if (bolded % 2) {225let node = document.createElement('b');226node.textContent = text;227return node;228}229return text;230})231);232listElement.appendChild(suggestion);233}234};235236// Default search engine is set to DuckDuckGo. Intended to work just like the usual237// bar at the top of a browser.238const getSearchTemplate = (239searchEngine = searchEngines[readStorage('SearchEngine')] ||240searchEngines[defaultSearch]241) => `https://${searchEngine}%s`,242// Like an omnibox, return the results of a search engine if search terms are243// provided instead of a URL.244search = (input) => {245try {246// Return the input if it is already a valid URL.247// eg: https://example.com, https://example.com/test?q=param248return new URL(input) + '';249} catch (e) {250// Continue if it is invalid.251}252253try {254// Check if the input is valid when http:// is added to the start.255// eg: example.com, https://example.com/test?q=param256const url = new URL(`http://${input}`);257// Return only if the hostname has a TLD or a subdomain.258if (url.hostname.indexOf('.') != -1) return url + '';259} catch (e) {260// Continue if it is invalid.261}262263// Treat the input as a search query instead of a website.264return getSearchTemplate().replace('%s', encodeURIComponent(input));265},266// Parse a URL to use with Ultraviolet.267uvUrl = (url) => {268try {269url = location.origin + uvConfig.prefix + uvConfig.encodeUrl(search(url));270} catch (e) {271// This is for cases where the Ultraviolet scripts have not been loaded.272url = search(url);273}274return url;275},276// Parse a URL to use with Scramjet.277sjUrl = (url) => {278try {279url = location.origin + sjEncode(search(url));280} catch (e) {281// This is for cases where the SJ scripts have not been loaded.282url = search(url);283}284return url;285},286rhUrl = async (url) =>287location.origin + (await RammerheadEncode(search(url)));288289/* RAMMERHEAD CONFIGURATION */290291// Store the search autocomplete string shuffler until reloaded.292// The ID must be a string containing 32 alphanumerical characters.293let rhACDict = { id: 'collectsearchautocompleteresults', dict: '' };294295// Parse a URL to use with Rammerhead. Only usable if the server is active.296const RammerheadEncode = async (baseUrl) => {297// Hellhead298const mod = (n, m) => ((n % m) + m) % m,299baseDictionary =300'0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz~-',301shuffledIndicator = '_rhs',302// Return a copy of the base dictionary with a randomized character order.303// Will be used as a Caesar cipher for URL encoding.304generateDictionary = () => {305let str = '';306const split = baseDictionary.split('');307while (split.length > 0) {308// Using .splice automatically rounds down to the nearest whole number.309str += split.splice(Math.random() * split.length, 1)[0];310}311return str;312};313314class StrShuffler {315constructor(dictionary = generateDictionary()) {316this.dictionary = dictionary;317}318319shuffle(str) {320// Do not reshuffle an already shuffled string.321if (!str.indexOf(shuffledIndicator)) return str;322323let shuffledStr = '';324for (let i = 0; i < str.length; i++) {325const char = str[i],326idx = baseDictionary.indexOf(char);327328/* For URL encoded characters and characters not included in the329* dictionary, leave untouched. Otherwise, replace with a character330* from the dictionary.331*/332if (char === '%' && str.length - i >= 3)333// A % symbol denotes that the next 2 characters are URL encoded.334shuffledStr += char + str[++i] + str[++i];335// Do not modify unrecognized characters.336else if (idx == -1) shuffledStr += char;337// Find the corresponding dictionary entry and use the character338// that is i places to the right of it.339else340shuffledStr += this.dictionary[mod(idx + i, baseDictionary.length)];341}342// Add a prefix signifying that the string has been shuffled.343return shuffledIndicator + shuffledStr;344}345346// Unshuffling is currently not done on the client side, and likely347// won't ever be for this implementation. It is used by the server instead.348unshuffle(str) {349// Do not unshuffle an already unshuffled string.350if (str.indexOf(shuffledIndicator)) return str;351352// Remove the prefix signifying that the string has been shuffled.353str = str.slice(shuffledIndicator.length);354355let unshuffledStr = '';356for (let i = 0; i < str.length; i++) {357const char = str[i],358idx = this.dictionary.indexOf(char);359360/* Convert the dictionary entry characters back into their base361* characters using the base dictionary. Again, leave URL encoded362* characters and unrecognized symbols alone.363*/364if (char === '%' && str.length - i >= 3)365unshuffledStr += char + str[++i] + str[++i];366else if (idx == -1) unshuffledStr += char;367// Find the corresponding base character entry and use the character368// that is i places to the left of it.369else370unshuffledStr += baseDictionary[mod(idx - i, baseDictionary.length)];371}372return unshuffledStr;373}374}375376// Request information that's beiing stored elsewhere on the server.377// Executes the callback function if the server responds as intended.378const get = (url, callback, shush = false) => {379let request = new XMLHttpRequest();380request.open('GET', url, true);381request.send();382383request.onerror = () => {384if (!shush) console.log('Cannot communicate with the server');385};386request.onload = () => {387if (request.status === 200) callback(request.responseText);388else if (!shush)389console.log(390`Unexpected server response to not match "200". Server says "${request.responseText}"`391);392};393},394// Functions for interacting with Rammerhead backend code on the server.395api = {396// Make a new Rammerhead session and do something with it.397newsession(callback) {398get('{{route}}{{/newsession}}', callback);399},400401// Check if a session with the specified ID exists, then do something.402sessionexists(id, callback) {403get(404'{{route}}{{/sessionexists}}?id=' + encodeURIComponent(id),405(res) => {406if (res === 'exists') return callback(true);407if (res === 'not found') return callback(false);408console.log('Unexpected response from server. Received ' + res);409}410);411},412413// Request a brand new encoding table to use for Rammerhead.414shuffleDict(id, callback) {415console.log('Shuffling', id);416get(417'{{route}}{{/api/shuffleDict}}?id=' + encodeURIComponent(id),418(res) => {419callback(JSON.parse(res));420}421);422},423},424/* Organize Rammerhead sessions via the browser's local storage.425* Local data consists of session creation timestamps and session IDs.426* The rest of the data is stored on the server.427*/428localStorageKey = 'rammerhead_sessionids',429localStorageKeyDefault = 'rammerhead_default_sessionid',430sessionIdsStore = {431// Get the local data of all stored sessions.432get() {433const rawData = localStorage.getItem(localStorageKey);434if (!rawData) return [];435try {436const data = JSON.parse(rawData);437438// Catch invalidly stored Rammerhead session data. Either that or439// it's poorly spoofed.440if (!Array.isArray(data)) throw 'getout';441return data;442} catch (e) {443return [];444}445},446447// Store local Rammerhead session data in the form of an array.448set(data) {449if (!Array.isArray(data)) throw new TypeError('Must be an array.');450localStorage.setItem(localStorageKey, JSON.stringify(data));451},452453// Get the default session data.454getDefault() {455const sessionId = localStorage.getItem(localStorageKeyDefault);456if (sessionId) {457let data = sessionIdsStore.get();458data.filter((session) => session.id === sessionId);459if (data.length) return data[0];460}461return null;462},463464// Set a new default session based on a given session ID.465setDefault(id) {466localStorage.setItem(localStorageKeyDefault, id);467},468},469// Store or update local data for a Rammerhead session, which consists of470// the session's ID and when the session was last created.471addSession = (id) => {472let data = sessionIdsStore.get();473data.unshift({ id: id, createdOn: new Date().toLocaleString() });474sessionIdsStore.set(data);475},476// Attempt to load an existing session that has been stored on the server.477getSessionId = (baseUrl) => {478return new Promise((resolve) => {479for (let i = 0; i < autocompleteUrls.length; i++)480if (baseUrl.indexOf(autocompleteUrls[i]) === 0)481return resolve(rhACDict.id);482// Check if the browser has stored an existing session.483const id = localStorage.getItem('session-string');484api.sessionexists(id, (value) => {485// Create a new session if Rammerhead can't find an existing session.486if (!value) {487console.log('Session validation failed');488api.newsession((id) => {489addSession(id);490localStorage.setItem('session-string', id);491console.log(id);492console.log('^ new id');493resolve(id);494});495}496// Load the stored session now that Rammerhead has found it.497else resolve(id);498});499});500};501502// Load the URL that was last visited in the Rammerhead session.503return getSessionId(baseUrl).then((id) => {504if (id === rhACDict.id && rhACDict.dict)505return new Promise((resolve) => {506resolve(507`{{route}}{{/}}${id}/` +508new StrShuffler(rhACDict.dict).shuffle(baseUrl)509);510});511return new Promise((resolve) => {512api.shuffleDict(id, (shuffleDict) => {513if (id === rhACDict.id) rhACDict.dict = shuffleDict;514// Encode the URL with Rammerhead's encoding table and return the URL.515resolve(516`{{route}}{{/}}${id}/` + new StrShuffler(shuffleDict).shuffle(baseUrl)517);518});519});520});521};522523/* To use:524* goProx.proxy(url-string, mode-as-string-or-number);525*526* Key: 1 = "stealth"527* 0 = "window"528* Nothing = return URL as a string529*530* Examples:531* Stealth mode -532* goProx.ultraviolet("https://google.com", 1);533* goProx.ultraviolet("https://google.com", "stealth");534*535* await goProx.rammerhead("https://google.com", 1);536* await goProx.rammerhead("https://google.com", "stealth");537*538* goProx.searx(1);539* goProx.searx("stealth");540*541* Window mode -542* goProx.ultraviolet("https://google.com", "window");543*544* await goProx.rammerhead("https://google.com", "window");545*546* goProx.searx("window");547*548* Return string value mode (default) -549* goProx.ultraviolet("https://google.com");550*551* await goProx.rammerhead("https://google.com");552*553* goProx.searx();554*/555const preparePage = async () => {556// This won't break the service workers as they store the variable separately.557uvConfig = self['{{__uv$config}}'];558sjObject = self['$scramjetLoadController'];559if (sjObject)560sjEncode = new (sjObject().ScramjetController)({561prefix: '{{route}}{{/scram/network/}}',562}).encodeUrl;563564// Object.freeze prevents goProx from accidentally being edited.565const goProx = Object.freeze({566// `location.protocol + "//" + getDomain()` more like `location.origin`567// setAuthCookie("__cor_auth=1", false);568ultraviolet: urlHandler(uvUrl),569570scramjet: urlHandler(sjUrl),571572rammerhead: asyncUrlHandler(rhUrl),573574terraria: urlHandler(location.protocol + '//a.' + getDomain()),575576webleste: urlHandler(location.protocol + '//b.' + getDomain()),577578osu: urlHandler(location.origin + '{{route}}{{/archive/osu}}'),579580agar: urlHandler(sjUrl('https://agar.io')),581582tru: urlHandler(sjUrl('https://truffled.lol/g')),583584prison: urlHandler(sjUrl('https://vimlark.itch.io/pick-up-prison')),585586speed: urlHandler(sjUrl('https://captain4lk.itch.io/what-the-road-brings')),587588heli: urlHandler(sjUrl('https://benjames171.itch.io/helo-storm')),589590youtube: urlHandler(uvUrl('https://michael.team/yt/')),591592invidious: urlHandler(sjUrl('https://invidious.snopyta.org')),593594freedomproject: urlHandler(sjUrl('https://0xdc.icu')),595596chatgpt: urlHandler(sjUrl('https://chat.openai.com/chat')),597598fmhy: urlHandler(sjUrl('https://fmhy.net')),599600discord: urlHandler(sjUrl('https://discord.com/app')),601602geforcenow: urlHandler(sjUrl('https://play.geforcenow.com/mall')),603604spotify: urlHandler(sjUrl('https://open.spotify.com')),605606tiktok: urlHandler(sjUrl('https://www.tiktok.com')),607608animetsu: urlHandler(sjUrl('https://animetsu.net')),609610twitter: urlHandler(sjUrl('https://twitter.com')),611612twitch: urlHandler(sjUrl('https://www.twitch.tv')),613614instagram: urlHandler(sjUrl('https://www.instagram.com')),615616reddit: urlHandler(sjUrl('https://www.reddit.com')),617618wikipedia: urlHandler(sjUrl('https://www.wikiwand.com')),619620});621622// Call a function after a given number of service workers are active.623// Workers are appended as additional arguments to the callback.624const callAfterWorkers = async (625urls,626callback,627afterHowMany = 1,628tries = 10,629...params630) => {631// For 10 tries, stop after 10 seconds of no response from workers.632if (tries <= 0) return console.log('Failed to recognize service workers.');633const workers = await Promise.all(634urls.map((url) => navigator.serviceWorker.getRegistration(url))635);636let newUrls = [],637finishedWorkers = [];638for (let i = 0; i < workers.length; i++) {639if (workers[i] && workers[i].active) {640afterHowMany--;641finishedWorkers.push(workers[i]);642} else newUrls.push(urls[i]);643}644if (afterHowMany <= 0) return await callback(...params, ...finishedWorkers);645else646await Promise.race([647navigator.serviceWorker.ready,648new Promise((resolve) => {649setTimeout(() => {650tries--;651resolve();652}, 1000);653}),654]);655return await callAfterWorkers(656newUrls,657callback,658afterHowMany,659tries,660...params,661...finishedWorkers662);663};664665// Attach event listeners using goProx to specific app menus that need it.666const prSet = (id, type) => {667const formElement = document.getElementById(id);668if (!formElement) return;669670let prUrl = formElement.querySelector('input[type=text]'),671prAC = formElement.querySelector('#autocomplete'),672prGo1 = document.querySelectorAll(`#${id}.pr-go1, #${id} .pr-go1`),673prGo2 = document.querySelectorAll(`#${id}.pr-go2, #${id} .pr-go2`);674675// Handle the other menu buttons differently if there is no omnibox. Menus676// which lack an omnibox likely use buttons as mere links.677const goProxMethod = prUrl678? (mode) => () => {679goProx[type](prUrl.value, mode);680}681: (mode) => () => {682goProx[type](mode);683},684// Ultraviolet and Scramjet are currently incompatible with window mode.685defaultModes = {686globalDefault: 'window',687ultraviolet: 'stealth',688scramjet: 'stealth',689rammerhead: 'window',690},691searchMode = defaultModes[type] || defaultModes['globalDefault'];692693if (prUrl) {694let enableSearch = false,695onCooldown = false;696697prUrl.addEventListener('keydown', async (e) => {698if (e.code === 'Enter') goProxMethod(searchMode)();699// This is exclusively used for the validator script.700else if (e.code === 'Validator Test') {701e.target.value = await goProx[type](e.target.value);702e.target.dispatchEvent(new Event('change'));703}704});705706if (prAC) {707// Set up a message channel to communicate with Scramjet, if it exists.708let autocompleteChannel = {},709sjLoaded = false;710if (sjObject) {711autocompleteChannel = new MessageChannel();712callAfterWorkers(['{{route}}{{/scram/scramjet.sw.js}}'], (worker) => {713worker.active.postMessage({ type: 'requestAC' }, [714autocompleteChannel.port2,715]);716sjLoaded = true;717});718719// Update the autocomplete results if Scramjet has processed them.720autocompleteChannel.port1.addEventListener('message', ({ data }) => {721updateAC(722prAC,723responseHandlers[data.searchType](data.responseJSON),724Date.parse(data.time)725);726sjLoaded = true;727});728729autocompleteChannel.port1.start();730}731732// Get autocomplete search results when typing in the omnibox.733prUrl.addEventListener('input', async (e) => {734// Prevent excessive fetch requests by restricting when requests are made.735if (enableSearch && !onCooldown) {736if (!e.target.value) {737prAC.textContent = '';738return;739}740const query = e.target.value;741if (e.isTrusted) {742onCooldown = true;743setTimeout(() => {744onCooldown = false;745// Refresh the autocomplete results after the cooldown ends.746if (query !== e.target.value)747e.target.dispatchEvent(new Event('input'));748}, 600);749}750751// Get autocomplete results from the selected search engine.752let searchType = readStorage('SearchEngine');753if (!(searchType in autocompletes)) searchType = defaultSearch;754const requestTime = new Date().toUTCString();755if (sjLoaded) {756sjLoaded = false;757requestAC('https://' + autocompletes[searchType], query, sjUrl, {758searchType: searchType,759port: autocompleteChannel.port1,760time: requestTime,761});762} else763requestAC('https://' + autocompletes[searchType], query, rhUrl, {764searchType: searchType,765prAC: prAC,766time: requestTime,767});768}769});770771// Show autocomplete results only if the omnibox is in focus.772prUrl.addEventListener('focus', () => {773// Don't show results if they were disabled by the user.774if (readStorage('UseAC') !== false) {775enableSearch = true;776prAC.classList.toggle('display-off', false);777}778prUrl.select();779});780prUrl.addEventListener('blur', (e) => {781enableSearch = false;782783// Do not remove the autocomplete result list if it was being clicked.784if (e.relatedTarget) {785e.relatedTarget.focus();786if (document.activeElement.parentNode === prAC) return;787}788789prAC.classList.toggle('display-off', true);790});791792// Make the corresponding search query if a given suggestion was clicked.793prAC.addEventListener('click', (e) => {794e.target.focus();795prUrl.value = document.activeElement.textContent;796goProxMethod(searchMode)();797});798}799}800801prGo1.forEach((element) => {802element.addEventListener('click', goProxMethod('window'));803});804prGo2.forEach((element) => {805element.addEventListener('click', goProxMethod('stealth'));806});807};808809prSet('pr-uv', 'ultraviolet');810prSet('pr-sj', 'scramjet');811prSet('pr-rh', 'rammerhead');812prSet('pr-yt', 'youtube');813prSet('pr-iv', 'invidious');814prSet('pr-trl', 'tru');815prSet('pr-fe', 'freedomproject');816prSet('pr-cg', 'chatgpt');817prSet('pr-fm', 'fmhy');818prSet('pr-dc', 'discord');819prSet('pr-gf', 'geforcenow');820prSet('pr-sp', 'spotify');821prSet('pr-tt', 'tiktok');822prSet('pr-ha', 'animetsu');823prSet('pr-tw', 'twitter');824prSet('pr-tc', 'twitch');825prSet('pr-ig', 'instagram');826prSet('pr-rt', 'reddit');827prSet('pr-wa', 'wikipedia');828829// Load the frame for stealth mode if it exists.830const windowFrame = document.getElementById('frame'),831loadFrame = () => {832windowFrame.src = localStorage.getItem('{{hu-lts}}-frame-url');833return true;834};835if (windowFrame) {836if (uvConfig && sjObject)837(await callAfterWorkers(838[839'{{route}}{{/scram/scramjet.sw.js}}',840'{{route}}{{/uv/sw.js}}',841'{{route}}{{/uv/sw-blacklist.js}}',842],843loadFrame,8442,8453846)) || loadFrame();847else loadFrame();848}849850const useModule = (moduleFunc, tries = 0) => {851try {852moduleFunc();853} catch (e) {854if (tries <= 5)855setTimeout(() => {856useModule(moduleFunc, tries + 1);857}, 600);858}859};860861if (document.getElementsByClassName('tippy-button').length >= 0)862useModule(() => {863tippy('.tippy-button', {864delay: 50,865animateFill: true,866placement: 'bottom',867});868});869if (document.getElementsByClassName('pr-tippy').length >= 0)870useModule(() => {871tippy('.pr-tippy', {872delay: 50,873animateFill: true,874placement: 'bottom',875});876});877878const banner = document.getElementById('banner');879if (banner) {880useModule(() => {881AOS.init();882});883884fetch('{{route}}{{/assets/json/splash.json}}', {885mode: 'same-origin',886}).then((response) => {887response.json().then((splashList) => {888banner.firstElementChild.innerHTML =889splashList[(Math.random() * splashList.length) | 0];890});891});892}893894// Load in relevant JSON files used to organize large sets of data.895// This first one is for links, whereas the rest are for navigation menus.896fetch('{{route}}{{/assets/json/links.json}}', {897mode: 'same-origin',898}).then((response) => {899response.json().then((huLinks) => {900for (let items = Object.entries(huLinks), i = 0; i < items.length; i++)901// Replace all placeholder links with the corresponding entry in huLinks.902(document.getElementById(items[i][0]) || {}).href = items[i][1];903});904});905906const navLists = {907// Pair an element ID with a JSON file name. They are identical for now.908'emu-nav': 'emu-nav',909'emulib-nav': 'emulib-nav',910'flash-nav': 'flash-nav',911'h5-nav': 'h5-nav',912'par-nav': 'par-nav',913};914915for (const [listId, filename] of Object.entries(navLists)) {916let navList = document.getElementById(listId);917918if (navList) {919// List items stored in JSON format will be returned as a JS object.920const data = await fetch(`{{route}}{{/assets/json/}}${filename}.json`, {921mode: 'same-origin',922}).then((response) => response.json());923924// Load the JSON lists into specific HTML parent elements as groups of925// child elements, if the parent element is found.926switch (filename) {927case 'emu-nav':928case 'emulib-nav':929case 'par-nav':930case 'h5-nav': {931const dirnames = {932// Set the directory of where each item of the corresponding JSON933// list will be retrieved from.934'emu-nav': 'emu',935'emulib-nav': 'emulib',936'par-nav': 'par',937'h5-nav': 'h5g',938},939dir = dirnames[filename],940// Add a little functionality for each list item when clicked on.941clickHandler = (parser, a) => (e) => {942if (e.target == a || e.target.tagName != 'A') {943e.preventDefault();944parser();945}946};947948for (let i = 0; i < data.length; i++) {949// Load each item as an anchor tag with an image, heading,950// and click event listener.951const item = data[i],952a = document.createElement('a'),953img = document.createElement('img'),954title = document.createElement('h3');955((desc = document.createElement('p')),956(credits = document.createElement('p')));957958a.href = '#';959img.src = `{{route}}{{/assets/img/}}${dir}/` + item.img;960title.textContent = item.name;961desc.textContent = item.description;962credits.textContent = item.credits;963964if (filename === 'par-nav') {965if (item.credits === 'truf')966desc.innerHTML +=967'<br>{{mask}}{{Credits: Check out the full site at }}<a target="_blank" href="{{route}}{{/truffled}}">{{mask}}{{truffled.lol}}</a> //{{mask}}{{ discord.gg/vVqY36mzvj}}';968}969970a.appendChild(img);971a.appendChild(title);972a.appendChild(desc);973974// Which function is used for the click event is determined by975// the corresponding location/index in the dirnames object.976const functionsList = [977// emu-nav978() => goFrame(item.path),979// emulib-nav980() =>981goFrame(982'{{route}}{{/webretro}}?core=' +983item.core +984'&rom=' +985item.rom986),987// par-nav988item.custom && goProx[item.custom]989? () => goProx[item.custom]('stealth')990: () => {},991// h5-nav992item.custom && goProx[item.custom]993? () => goProx[item.custom]('window')994: () => goFrame('{{route}}{{/archive/g/}}' + item.path),995];996997a.addEventListener(998'click',999clickHandler(1000functionsList[Object.values(dirnames).indexOf(dir)],1001a1002)1003);10041005navList.appendChild(a);1006}1007break;1008}10091010case 'flash-nav':1011for (let i = 0; i < data.length; i++) {1012// Load each item as an anchor tag with a short title and click1013// event listener.1014const item = data[i],1015a = document.createElement('a');1016a.href = '#';1017a.textContent = item.slice(0, -4);10181019a.addEventListener('click', (e) => {1020e.preventDefault();1021goFrame('{{route}}{{/flash}}?swf=' + item);1022});10231024navList.appendChild(a);1025}1026break;10271028// No default case.1029}1030}1031}10321033const isTopLevel = window.self === window.top;1034if (isTopLevel) {1035const launchType = readStorage('LaunchType');1036let newWindow = null;10371038if (launchType === 'blank') {1039newWindow = openBlankCloak();1040} else if (launchType === 'blob') {1041newWindow = openBlobCloak();1042}10431044if (newWindow) {1045window.location.replace('about:blank');1046setTimeout(() => {1047window.close();1048}, 100);1049}1050}1051};1052if ('loading' === document.readyState)1053addEventListener('DOMContentLoaded', preparePage);1054else preparePage();1055})();105610571058