Path: blob/master/views/assets/js/common-1735118314.js
5247 views
/* -----------------------------------------------1/* Authors: QuiteAFancyEmerald, Yoct, and OlyB2/* GNU Affero General Public License v3.0: https://www.gnu.org/licenses/agpl-3.0.en.html3/* MAIN Holy Unblocker 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|edu|cooking|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},49// An asynchronous version of the function above, just in case.50asyncUrlHandler = (parser) => async (url, mode) => {51if (!url) return;52if (typeof parser === 'function') url = await parser(url);53mode = `${mode}`.toLowerCase();54if (mode === 'stealth' || mode == 1) goFrame(url);55else if (mode === 'window' || mode == 0) location.href = url;56else return url;57};5859/* READ SETTINGS */6061const storageId = '{{hu-lts}}-storage',62storageObject = () => JSON.parse(localStorage.getItem(storageId)) || {},63readStorage = (name) => storageObject()[name];6465/* OMNIBOX */6667const searchEngines = Object.freeze({68'{{Startpage}}': 'startpage.com/sp/search?query=',69'{{Google}}': 'google.com/search?q=',70'{{Bing}}': 'bing.com/search?q=',71'{{DuckDuckGo}}': 'duckduckgo.com/?q=',72'{{Brave}}': 'search.brave.com/search?q=',73}),74defaultSearch = '{{defaultSearch}}',75autocompletes = Object.freeze({76// Startpage has used both Google's and Bing's autocomplete.77// For now, just use Bing.78'{{Startpage}}': 'www.bing.com/AS/Suggestions?csr=1&cvid=0&qry=',79'{{Google}}': 'www.google.com/complete/search?client=gws-wiz&callback=_&q=',80'{{Bing}}': 'www.bing.com/AS/Suggestions?csr=1&cvid=0&qry=',81'{{DuckDuckGo}}': 'duckduckgo.com/ac/?q=',82'{{Brave}}': 'search.brave.com/api/suggest?q=',83}),84autocompleteUrls = Object.values(autocompletes).map(85(url) => 'https://' + url86),87responseDelimiter = '\ue000',88formatSuggestion = (89suggestion,90delimiters,91newDelimiters = [responseDelimiter]92) => {93for (let i = 0; i < delimiters.length; i++)94suggestion = suggestion.replaceAll(95delimiters[i],96newDelimiters[i] || newDelimiters[0]97);98return suggestion;99},100responseHandlers = Object.freeze({101'{{Startpage}}': (jsonData) => responseHandlers['{{Bing}}'](jsonData),102'{{Google}}': (jsonData) =>103jsonData[0].map(([suggestion]) =>104formatSuggestion(suggestion, ['<b>', '</b>'])105),106'{{Bing}}': (jsonData) =>107jsonData.s.map(({ q }) => formatSuggestion(q, ['\ue000', '\ue001'])),108'{{DuckDuckGo}}': (jsonData) => jsonData.map(({ phrase }) => phrase),109'{{Brave}}': (jsonData) => jsonData[1],110});111112// Get the autocomplete results for a given search query in JSON format.113const requestAC = async (114baseUrl,115query,116parserFunc = (url) => url,117params = {}118) => {119switch (parserFunc) {120case sjUrl: {121// Ask Scramjet to process the autocomplete request. Processed results will122// be returned to an event handler and are updated from there.123params.port.postMessage({124type: params.searchType,125request: {126url: parserFunc(baseUrl + encodeURIComponent(query)),127headers: new Map([['Date', params.time]]),128},129});130break;131}132case rhUrl: {133// Have Rammerhead process the autocomplete request.134const response = await fetch(135await parserFunc(baseUrl + encodeURIComponent(query))136),137responseType = response.headers.get('content-type');138let responseJSON = {};139if (responseType && responseType.indexOf('application/json') !== -1)140responseJSON = await response.json();141else142try {143responseJSON = await response.text();144try {145responseJSON = responseJSON.match(146/(?<=\/\*hammerhead\|.*header-end\*\/)[^]+?(?=\/\*hammerhead\|.*end\*\/)/i147)[0];148} catch (e) {149// In case Rammerhead chose not to encode the response.150}151try {152responseJSON = JSON.parse(responseJSON);153} catch (e) {154responseJSON = JSON.parse(155responseJSON.replace(/^[^[{]*|[^\]}]*$/g, '')156);157}158} catch (e) {159// responseJSON will be an empty object if everything was invalid.160}161162// Update the autocomplete results directly.163updateAC(164params.prAC,165responseHandlers[params.searchType](responseJSON),166Date.parse(params.time)167);168break;169}170}171};172173let lastUpdated = Date.parse(new Date().toUTCString());174const updateAC = (listElement, searchResults, time) => {175if (time < lastUpdated) return;176else lastUpdated = time;177// Update the data for the results.178listElement.textContent = '';179for (let i = 0; i < searchResults.length; i++) {180let suggestion = document.createElement('li');181suggestion.tabIndex = 0;182suggestion.append(183...searchResults[i].split(responseDelimiter).map((text, bolded) => {184if (bolded % 2) {185let node = document.createElement('b');186node.textContent = text;187return node;188}189return text;190})191);192listElement.appendChild(suggestion);193}194};195196// Default search engine is set to Google. Intended to work just like the usual197// bar at the top of a browser.198const getSearchTemplate = (199searchEngine = searchEngines[readStorage('SearchEngine')] ||200searchEngines[defaultSearch]201) => `https://${searchEngine}%s`,202// Like an omnibox, return the results of a search engine if search terms are203// provided instead of a URL.204search = (input) => {205try {206// Return the input if it is already a valid URL.207// eg: https://example.com, https://example.com/test?q=param208return new URL(input) + '';209} catch (e) {210// Continue if it is invalid.211}212213try {214// Check if the input is valid when http:// is added to the start.215// eg: example.com, https://example.com/test?q=param216const url = new URL(`http://${input}`);217// Return only if the hostname has a TLD or a subdomain.218if (url.hostname.indexOf('.') != -1) return url + '';219} catch (e) {220// Continue if it is invalid.221}222223// Treat the input as a search query instead of a website.224return getSearchTemplate().replace('%s', encodeURIComponent(input));225},226// Parse a URL to use with Ultraviolet.227uvUrl = (url) => {228try {229url = location.origin + uvConfig.prefix + uvConfig.encodeUrl(search(url));230} catch (e) {231// This is for cases where the Ultraviolet scripts have not been loaded.232url = search(url);233}234return url;235},236// Parse a URL to use with Scramjet.237sjUrl = (url) => {238try {239url = location.origin + sjEncode(search(url));240} catch (e) {241// This is for cases where the SJ scripts have not been loaded.242url = search(url);243}244return url;245},246rhUrl = async (url) =>247location.origin + (await RammerheadEncode(search(url)));248249/* RAMMERHEAD CONFIGURATION */250251// Store the search autocomplete string shuffler until reloaded.252// The ID must be a string containing 32 alphanumerical characters.253let rhACDict = { id: 'collectsearchautocompleteresults', dict: '' };254255// Parse a URL to use with Rammerhead. Only usable if the server is active.256const RammerheadEncode = async (baseUrl) => {257// Hellhead258const mod = (n, m) => ((n % m) + m) % m,259baseDictionary =260'0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz~-',261shuffledIndicator = '_rhs',262// Return a copy of the base dictionary with a randomized character order.263// Will be used as a Caesar cipher for URL encoding.264generateDictionary = () => {265let str = '';266const split = baseDictionary.split('');267while (split.length > 0) {268// Using .splice automatically rounds down to the nearest whole number.269str += split.splice(Math.random() * split.length, 1)[0];270}271return str;272};273274class StrShuffler {275constructor(dictionary = generateDictionary()) {276this.dictionary = dictionary;277}278279shuffle(str) {280// Do not reshuffle an already shuffled string.281if (!str.indexOf(shuffledIndicator)) return str;282283let shuffledStr = '';284for (let i = 0; i < str.length; i++) {285const char = str[i],286idx = baseDictionary.indexOf(char);287288/* For URL encoded characters and characters not included in the289* dictionary, leave untouched. Otherwise, replace with a character290* from the dictionary.291*/292if (char === '%' && str.length - i >= 3)293// A % symbol denotes that the next 2 characters are URL encoded.294shuffledStr += char + str[++i] + str[++i];295// Do not modify unrecognized characters.296else if (idx == -1) shuffledStr += char;297// Find the corresponding dictionary entry and use the character298// that is i places to the right of it.299else300shuffledStr += this.dictionary[mod(idx + i, baseDictionary.length)];301}302// Add a prefix signifying that the string has been shuffled.303return shuffledIndicator + shuffledStr;304}305306// Unshuffling is currently not done on the client side, and likely307// won't ever be for this implementation. It is used by the server instead.308unshuffle(str) {309// Do not unshuffle an already unshuffled string.310if (str.indexOf(shuffledIndicator)) return str;311312// Remove the prefix signifying that the string has been shuffled.313str = str.slice(shuffledIndicator.length);314315let unshuffledStr = '';316for (let i = 0; i < str.length; i++) {317const char = str[i],318idx = this.dictionary.indexOf(char);319320/* Convert the dictionary entry characters back into their base321* characters using the base dictionary. Again, leave URL encoded322* characters and unrecognized symbols alone.323*/324if (char === '%' && str.length - i >= 3)325unshuffledStr += char + str[++i] + str[++i];326else if (idx == -1) unshuffledStr += char;327// Find the corresponding base character entry and use the character328// that is i places to the left of it.329else330unshuffledStr += baseDictionary[mod(idx - i, baseDictionary.length)];331}332return unshuffledStr;333}334}335336// Request information that's beiing stored elsewhere on the server.337// Executes the callback function if the server responds as intended.338const get = (url, callback, shush = false) => {339let request = new XMLHttpRequest();340request.open('GET', url, true);341request.send();342343request.onerror = () => {344if (!shush) console.log('Cannot communicate with the server');345};346request.onload = () => {347if (request.status === 200) callback(request.responseText);348else if (!shush)349console.log(350`Unexpected server response to not match "200". Server says "${request.responseText}"`351);352};353},354// Functions for interacting with Rammerhead backend code on the server.355api = {356// Make a new Rammerhead session and do something with it.357newsession(callback) {358get('{{route}}{{/newsession}}', callback);359},360361// Check if a session with the specified ID exists, then do something.362sessionexists(id, callback) {363get(364'{{route}}{{/sessionexists}}?id=' + encodeURIComponent(id),365(res) => {366if (res === 'exists') return callback(true);367if (res === 'not found') return callback(false);368console.log('Unexpected response from server. Received ' + res);369}370);371},372373// Request a brand new encoding table to use for Rammerhead.374shuffleDict(id, callback) {375console.log('Shuffling', id);376get(377'{{route}}{{/api/shuffleDict}}?id=' + encodeURIComponent(id),378(res) => {379callback(JSON.parse(res));380}381);382},383},384/* Organize Rammerhead sessions via the browser's local storage.385* Local data consists of session creation timestamps and session IDs.386* The rest of the data is stored on the server.387*/388localStorageKey = 'rammerhead_sessionids',389localStorageKeyDefault = 'rammerhead_default_sessionid',390sessionIdsStore = {391// Get the local data of all stored sessions.392get() {393const rawData = localStorage.getItem(localStorageKey);394if (!rawData) return [];395try {396const data = JSON.parse(rawData);397398// Catch invalidly stored Rammerhead session data. Either that or399// it's poorly spoofed.400if (!Array.isArray(data)) throw 'getout';401return data;402} catch (e) {403return [];404}405},406407// Store local Rammerhead session data in the form of an array.408set(data) {409if (!Array.isArray(data)) throw new TypeError('Must be an array.');410localStorage.setItem(localStorageKey, JSON.stringify(data));411},412413// Get the default session data.414getDefault() {415const sessionId = localStorage.getItem(localStorageKeyDefault);416if (sessionId) {417let data = sessionIdsStore.get();418data.filter((session) => session.id === sessionId);419if (data.length) return data[0];420}421return null;422},423424// Set a new default session based on a given session ID.425setDefault(id) {426localStorage.setItem(localStorageKeyDefault, id);427},428},429// Store or update local data for a Rammerhead session, which consists of430// the session's ID and when the session was last created.431addSession = (id) => {432let data = sessionIdsStore.get();433data.unshift({ id: id, createdOn: new Date().toLocaleString() });434sessionIdsStore.set(data);435},436// Attempt to load an existing session that has been stored on the server.437getSessionId = (baseUrl) => {438return new Promise((resolve) => {439for (let i = 0; i < autocompleteUrls.length; i++)440if (baseUrl.indexOf(autocompleteUrls[i]) === 0)441return resolve(rhACDict.id);442// Check if the browser has stored an existing session.443const id = localStorage.getItem('session-string');444api.sessionexists(id, (value) => {445// Create a new session if Rammerhead can't find an existing session.446if (!value) {447console.log('Session validation failed');448api.newsession((id) => {449addSession(id);450localStorage.setItem('session-string', id);451console.log(id);452console.log('^ new id');453resolve(id);454});455}456// Load the stored session now that Rammerhead has found it.457else resolve(id);458});459});460};461462// Load the URL that was last visited in the Rammerhead session.463return getSessionId(baseUrl).then((id) => {464if (id === rhACDict.id && rhACDict.dict)465return new Promise((resolve) => {466resolve(467`{{route}}{{/}}${id}/` +468new StrShuffler(rhACDict.dict).shuffle(baseUrl)469);470});471return new Promise((resolve) => {472api.shuffleDict(id, (shuffleDict) => {473if (id === rhACDict.id) rhACDict.dict = shuffleDict;474// Encode the URL with Rammerhead's encoding table and return the URL.475resolve(476`{{route}}{{/}}${id}/` + new StrShuffler(shuffleDict).shuffle(baseUrl)477);478});479});480});481};482483/* To use:484* goProx.proxy(url-string, mode-as-string-or-number);485*486* Key: 1 = "stealth"487* 0 = "window"488* Nothing = return URL as a string489*490* Examples:491* Stealth mode -492* goProx.ultraviolet("https://google.com", 1);493* goProx.ultraviolet("https://google.com", "stealth");494*495* await goProx.rammerhead("https://google.com", 1);496* await goProx.rammerhead("https://google.com", "stealth");497*498* goProx.searx(1);499* goProx.searx("stealth");500*501* Window mode -502* goProx.ultraviolet("https://google.com", "window");503*504* await goProx.rammerhead("https://google.com", "window");505*506* goProx.searx("window");507*508* Return string value mode (default) -509* goProx.ultraviolet("https://google.com");510*511* await goProx.rammerhead("https://google.com");512*513* goProx.searx();514*/515const preparePage = async () => {516// This won't break the service workers as they store the variable separately.517uvConfig = self['{{__uv$config}}'];518sjObject = self['$scramjetLoadController'];519if (sjObject)520sjEncode = new (sjObject().ScramjetController)({521prefix: '{{route}}{{/scram/network/}}',522}).encodeUrl;523524// Object.freeze prevents goProx from accidentally being edited.525const goProx = Object.freeze({526// `location.protocol + "//" + getDomain()` more like `location.origin`527// setAuthCookie("__cor_auth=1", false);528ultraviolet: urlHandler(uvUrl),529530scramjet: urlHandler(sjUrl),531532rammerhead: asyncUrlHandler(rhUrl),533534searx: urlHandler(location.protocol + `//c.${getDomain()}/engine/`),535536libreddit: urlHandler(location.protocol + '//c.' + getDomain()),537538rnav: urlHandler(location.protocol + '//client.' + getDomain()),539540osu: urlHandler(location.origin + '{{route}}{{/archive/osu}}'),541542agar: urlHandler(sjUrl('https://agar.io')),543544tru: urlHandler(sjUrl('https://truffled.lol/g')),545546prison: urlHandler(sjUrl('https://vimlark.itch.io/pick-up-prison')),547548speed: urlHandler(sjUrl('https://captain4lk.itch.io/what-the-road-brings')),549550heli: urlHandler(sjUrl('https://benjames171.itch.io/helo-storm')),551552youtube: urlHandler(sjUrl('https://youtube.com')),553554invidious: urlHandler(sjUrl('https://invidious.snopyta.org')),555556chatgpt: urlHandler(sjUrl('https://chat.openai.com/chat')),557558discord: urlHandler(sjUrl('https://discord.com/app')),559560geforcenow: urlHandler(sjUrl('https://play.geforcenow.com/mall')),561562spotify: urlHandler(sjUrl('https://open.spotify.com')),563564tiktok: urlHandler(sjUrl('https://www.tiktok.com')),565566hianime: urlHandler(sjUrl('https://www.hianime.to')),567568twitter: urlHandler(sjUrl('https://twitter.com')),569570twitch: urlHandler(sjUrl('https://www.twitch.tv')),571572instagram: urlHandler(sjUrl('https://www.instagram.com')),573574reddit: urlHandler(sjUrl('https://www.reddit.com')),575576wikipedia: urlHandler(sjUrl('https://www.wikiwand.com')),577578newgrounds: urlHandler(sjUrl('https://www.newgrounds.com')),579});580581// Call a function after a given number of service workers are active.582// Workers are appended as additional arguments to the callback.583const callAfterWorkers = async (584urls,585callback,586afterHowMany = 1,587tries = 10,588...params589) => {590// For 10 tries, stop after 10 seconds of no response from workers.591if (tries <= 0) return console.log('Failed to recognize service workers.');592const workers = await Promise.all(593urls.map((url) => navigator.serviceWorker.getRegistration(url))594);595let newUrls = [],596finishedWorkers = [];597for (let i = 0; i < workers.length; i++) {598if (workers[i] && workers[i].active) {599afterHowMany--;600finishedWorkers.push(workers[i]);601} else newUrls.push(urls[i]);602}603if (afterHowMany <= 0) return await callback(...params, ...finishedWorkers);604else605await Promise.race([606navigator.serviceWorker.ready,607new Promise((resolve) => {608setTimeout(() => {609tries--;610resolve();611}, 1000);612}),613]);614return await callAfterWorkers(615newUrls,616callback,617afterHowMany,618tries,619...params,620...finishedWorkers621);622};623624// Attach event listeners using goProx to specific app menus that need it.625const prSet = (id, type) => {626const formElement = document.getElementById(id);627if (!formElement) return;628629let prUrl = formElement.querySelector('input[type=text]'),630prAC = formElement.querySelector('#autocomplete'),631prGo1 = document.querySelectorAll(`#${id}.pr-go1, #${id} .pr-go1`),632prGo2 = document.querySelectorAll(`#${id}.pr-go2, #${id} .pr-go2`);633634// Handle the other menu buttons differently if there is no omnibox. Menus635// which lack an omnibox likely use buttons as mere links.636const goProxMethod = prUrl637? (mode) => () => {638goProx[type](prUrl.value, mode);639}640: (mode) => () => {641goProx[type](mode);642},643// Ultraviolet and Scramjet are currently incompatible with window mode.644defaultModes = {645globalDefault: 'window',646ultraviolet: 'stealth',647scramjet: 'stealth',648rammerhead: 'window',649},650searchMode = defaultModes[type] || defaultModes['globalDefault'];651652if (prUrl) {653let enableSearch = false,654onCooldown = false;655656prUrl.addEventListener('keydown', async (e) => {657if (e.code === 'Enter') goProxMethod(searchMode)();658// This is exclusively used for the validator script.659else if (e.code === 'Validator Test') {660e.target.value = await goProx[type](e.target.value);661e.target.dispatchEvent(new Event('change'));662}663});664665if (prAC) {666// Set up a message channel to communicate with Scramjet, if it exists.667let autocompleteChannel = {},668sjLoaded = false;669if (sjObject) {670autocompleteChannel = new MessageChannel();671callAfterWorkers(['{{route}}{{/scram/scramjet.sw.js}}'], (worker) => {672worker.active.postMessage({ type: 'requestAC' }, [673autocompleteChannel.port2,674]);675sjLoaded = true;676});677678// Update the autocomplete results if Scramjet has processed them.679autocompleteChannel.port1.addEventListener('message', ({ data }) => {680updateAC(681prAC,682responseHandlers[data.searchType](data.responseJSON),683Date.parse(data.time)684);685sjLoaded = true;686});687688autocompleteChannel.port1.start();689}690691// Get autocomplete search results when typing in the omnibox.692prUrl.addEventListener('input', async (e) => {693// Prevent excessive fetch requests by restricting when requests are made.694if (enableSearch && !onCooldown) {695if (!e.target.value) {696prAC.textContent = '';697return;698}699const query = e.target.value;700if (e.isTrusted) {701onCooldown = true;702setTimeout(() => {703onCooldown = false;704// Refresh the autocomplete results after the cooldown ends.705if (query !== e.target.value)706e.target.dispatchEvent(new Event('input'));707}, 600);708}709710// Get autocomplete results from the selected search engine.711let searchType = readStorage('SearchEngine');712if (!(searchType in autocompletes)) searchType = defaultSearch;713const requestTime = new Date().toUTCString();714if (sjLoaded) {715sjLoaded = false;716requestAC('https://' + autocompletes[searchType], query, sjUrl, {717searchType: searchType,718port: autocompleteChannel.port1,719time: requestTime,720});721} else722requestAC('https://' + autocompletes[searchType], query, rhUrl, {723searchType: searchType,724prAC: prAC,725time: requestTime,726});727}728});729730// Show autocomplete results only if the omnibox is in focus.731prUrl.addEventListener('focus', () => {732// Don't show results if they were disabled by the user.733if (readStorage('UseAC') !== false) {734enableSearch = true;735prAC.classList.toggle('display-off', false);736}737prUrl.select();738});739prUrl.addEventListener('blur', (e) => {740enableSearch = false;741742// Do not remove the autocomplete result list if it was being clicked.743if (e.relatedTarget) {744e.relatedTarget.focus();745if (document.activeElement.parentNode === prAC) return;746}747748prAC.classList.toggle('display-off', true);749});750751// Make the corresponding search query if a given suggestion was clicked.752prAC.addEventListener('click', (e) => {753e.target.focus();754prUrl.value = document.activeElement.textContent;755goProxMethod(searchMode)();756});757}758}759760prGo1.forEach((element) => {761element.addEventListener('click', goProxMethod('window'));762});763prGo2.forEach((element) => {764element.addEventListener('click', goProxMethod('stealth'));765});766};767768prSet('pr-uv', 'ultraviolet');769prSet('pr-sj', 'scramjet');770prSet('pr-rh', 'rammerhead');771prSet('pr-yt', 'youtube');772prSet('pr-iv', 'invidious');773prSet('pr-cg', 'chatgpt');774prSet('pr-dc', 'discord');775prSet('pr-gf', 'geforcenow');776prSet('pr-sp', 'spotify');777prSet('pr-tt', 'tiktok');778prSet('pr-ha', 'hianime');779prSet('pr-tw', 'twitter');780prSet('pr-tc', 'twitch');781prSet('pr-ig', 'instagram');782prSet('pr-rt', 'reddit');783prSet('pr-wa', 'wikipedia');784prSet('pr-ng', 'newgrounds');785786// Load the frame for stealth mode if it exists.787const windowFrame = document.getElementById('frame'),788loadFrame = () => {789windowFrame.src = localStorage.getItem('{{hu-lts}}-frame-url');790return true;791};792if (windowFrame) {793if (uvConfig && sjObject)794(await callAfterWorkers(795[796'{{route}}{{/scram/scramjet.sw.js}}',797'{{route}}{{/uv/sw.js}}',798'{{route}}{{/uv/sw-blacklist.js}}',799],800loadFrame,8012,8023803)) || loadFrame();804else loadFrame();805}806807const useModule = (moduleFunc, tries = 0) => {808try {809moduleFunc();810} catch (e) {811if (tries <= 5)812setTimeout(() => {813useModule(moduleFunc, tries + 1);814}, 600);815}816};817818if (document.getElementsByClassName('tippy-button').length >= 0)819useModule(() => {820tippy('.tippy-button', {821delay: 50,822animateFill: true,823placement: 'bottom',824});825});826if (document.getElementsByClassName('pr-tippy').length >= 0)827useModule(() => {828tippy('.pr-tippy', {829delay: 50,830animateFill: true,831placement: 'bottom',832});833});834835const banner = document.getElementById('banner');836if (banner) {837useModule(() => {838AOS.init();839});840841fetch('{{route}}{{/assets/json/splash.json}}', {842mode: 'same-origin',843}).then((response) => {844response.json().then((splashList) => {845banner.firstElementChild.innerHTML =846splashList[(Math.random() * splashList.length) | 0];847});848});849}850851// Load in relevant JSON files used to organize large sets of data.852// This first one is for links, whereas the rest are for navigation menus.853fetch('{{route}}{{/assets/json/links.json}}', {854mode: 'same-origin',855}).then((response) => {856response.json().then((huLinks) => {857for (let items = Object.entries(huLinks), i = 0; i < items.length; i++)858// Replace all placeholder links with the corresponding entry in huLinks.859(document.getElementById(items[i][0]) || {}).href = items[i][1];860});861});862863const navLists = {864// Pair an element ID with a JSON file name. They are identical for now.865'emu-nav': 'emu-nav',866'emulib-nav': 'emulib-nav',867'flash-nav': 'flash-nav',868'h5-nav': 'h5-nav',869'par-nav': 'par-nav',870};871872for (const [listId, filename] of Object.entries(navLists)) {873let navList = document.getElementById(listId);874875if (navList) {876// List items stored in JSON format will be returned as a JS object.877const data = await fetch(`{{route}}{{/assets/json/}}${filename}.json`, {878mode: 'same-origin',879}).then((response) => response.json());880881// Load the JSON lists into specific HTML parent elements as groups of882// child elements, if the parent element is found.883switch (filename) {884case 'emu-nav':885case 'emulib-nav':886case 'par-nav':887case 'h5-nav': {888const dirnames = {889// Set the directory of where each item of the corresponding JSON890// list will be retrieved from.891'emu-nav': 'emu',892'emulib-nav': 'emulib',893'par-nav': 'par',894'h5-nav': 'h5g',895},896dir = dirnames[filename],897// Add a little functionality for each list item when clicked on.898clickHandler = (parser, a) => (e) => {899if (e.target == a || e.target.tagName != 'A') {900e.preventDefault();901parser();902}903};904905for (let i = 0; i < data.length; i++) {906// Load each item as an anchor tag with an image, heading,907// and click event listener.908const item = data[i],909a = document.createElement('a'),910img = document.createElement('img'),911title = document.createElement('h3');912((desc = document.createElement('p')),913(credits = document.createElement('p')));914915a.href = '#';916img.src = `{{route}}{{/assets/img/}}${dir}/` + item.img;917title.textContent = item.name;918desc.textContent = item.description;919credits.textContent = item.credits;920921if (filename === 'par-nav') {922if (item.credits === 'truf')923desc.innerHTML +=924'<br>{{mask}}{{Credits: Check out the full site at }}<a target="_blank" href="{{route}}{{/truffled}}">{{mask}}{{truffled.lol}}</a> //{{mask}}{{ discord.gg/vVqY36mzvj}}';925}926927a.appendChild(img);928a.appendChild(title);929a.appendChild(desc);930931// Which function is used for the click event is determined by932// the corresponding location/index in the dirnames object.933const functionsList = [934// emu-nav935() => goFrame(item.path),936// emulib-nav937() =>938goFrame(939'{{route}}{{/webretro}}?core=' +940item.core +941'&rom=' +942item.rom943),944// par-nav945item.custom && goProx[item.custom]946? () => goProx[item.custom]('stealth')947: () => {},948// h5-nav949item.custom && goProx[item.custom]950? () => goProx[item.custom]('stealth')951: () => goFrame('{{route}}{{/archive/g/}}' + item.path),952];953954a.addEventListener(955'click',956clickHandler(957functionsList[Object.values(dirnames).indexOf(dir)],958a959)960);961962navList.appendChild(a);963}964break;965}966967case 'flash-nav':968for (let i = 0; i < data.length; i++) {969// Load each item as an anchor tag with a short title and click970// event listener.971const item = data[i],972a = document.createElement('a');973a.href = '#';974a.textContent = item.slice(0, -4);975976a.addEventListener('click', (e) => {977e.preventDefault();978goFrame('{{route}}{{/flash}}?swf=' + item);979});980981navList.appendChild(a);982}983break;984985// No default case.986}987}988}989};990if ('loading' === document.readyState)991addEventListener('DOMContentLoaded', preparePage);992else preparePage();993})();994995996