diff --git a/views/pages/emulator.ejs b/views/pages/emulator.ejs index 25107c3..aafdce4 100644 --- a/views/pages/emulator.ejs +++ b/views/pages/emulator.ejs @@ -51,360 +51,36 @@ - - + + \ No newline at end of file diff --git a/views/pages/emulators.ejs b/views/pages/emulators.ejs index 1fe43a4..eccdc59 100644 --- a/views/pages/emulators.ejs +++ b/views/pages/emulators.ejs @@ -2,6 +2,7 @@ +
@@ -52,255 +53,23 @@
   
- - \ No newline at end of file + +<% if (typeof emulators !== 'undefined' && emulators) { %> + +<% } %> \ No newline at end of file diff --git a/views/pages/error.ejs b/views/pages/error.ejs index be2d761..0922045 100644 --- a/views/pages/error.ejs +++ b/views/pages/error.ejs @@ -6,105 +6,7 @@ - +
diff --git a/views/pages/settings.ejs b/views/pages/settings.ejs index ed8150e..24aa745 100644 --- a/views/pages/settings.ejs +++ b/views/pages/settings.ejs @@ -66,108 +66,5 @@ defaults = <%-JSON.stringify(defaultSettings)%> settingStore = localStorage.getItem('settings') settings = undefined - - function setBoosts(){ - for(let boost in defaults.boost){ - if(typeof settings.boost[boost] == 'undefined') {settings.boost[boost] == defaults.boost[boost]} - document.getElementById(boost + 'boost').value = settings.boost[boost] - document.getElementById(boost + 'boost').addEventListener('keyup', () => { - validate(document.getElementById(boost + 'boost')) - }) - } - } - - function setColumns(){ - for(let field in settings.fields){ - let element = document.getElementById(settings.fields[field]) - if(!element){settings.fields.splice(field, 1)} - else{element.checked = true} - } - for(let field in defaults.fields){ - document.getElementById(defaults.fields[field]).addEventListener('change', () => { - toggleLinkedTextBox(document.getElementById(defaults.fields[field]), document.getElementById(defaults.fields[field] + 'boost')) - }) - let elem = document.getElementById(defaults.fields[field]) - if(!elem.checked){ - let boostField = document.getElementById(defaults.fields[field] + 'boost') - boostField.classList.add('text-secondary') - boostField.disabled = true - } - } - } - - function setOthers(){ - if(typeof settings.combineWith == 'undefined') {settings.combineWith = defaults.combineWith} - if(typeof settings.fuzzy == 'undefined') {settings.fuzzy = defaults.fuzzy} - if(typeof settings.prefix == 'undefined') {settings.prefix = defaults.prefix} - if(typeof settings.hideNonGame == 'undefined') {settings.hideNonGame = defaults.hideNonGame} - document.getElementById('combineWith').checked = settings.combineWith ? true : false - document.getElementById('fuzzy').value = settings.fuzzy - document.getElementById('prefix').checked = settings.prefix - document.getElementById('hideNonGame').checked = settings.hideNonGame - } - - function saveSettings(){ - for(let boost in defaults.boost){settings.boost[boost] = parseInt(document.getElementById(boost + 'boost').value)} - settings.fields = [] - for(let field in defaults.fields){ - if(document.getElementById(defaults.fields[field]).checked){ - settings.fields.push(defaults.fields[field]) - } - } - settings.combineWith = document.getElementById('combineWith').checked ? 'AND' : '' - settings.fuzzy = parseFloat (document.getElementById('fuzzy').value) - settings.prefix = document.getElementById('prefix').checked - settings.hideNonGame = document.getElementById('hideNonGame').checked - localStorage.setItem('settings', JSON.stringify(settings)) - window.location.href = '/' - } - - function loadSettings(){ - $(function () { - $('[data-toggle="tooltip"]').tooltip() - }) - if(!settingStore) { - settings = structuredClone(defaults) - settingStore = JSON.stringify(settings) - localStorage.setItem('settings', settingStore) - } - else{ - try{ - settings = JSON.parse(settingStore) - } - catch{ - //load defaults if not exist - settings = defaults - } - } - setBoosts() - setColumns() - setOthers() - } - document.body.onload = loadSettings - document.getElementById('saveSettings').onclick = saveSettings - - function validate(element){ - let max = parseInt(element.max) - let min = parseInt(element.min) - let value = parseInt(element.value) - if(value > max) {element.value = max} - if(value < min) {element.value = min} - console.log(max, min, value) - } - fuzzyElem = document.getElementById('fuzzy') - fuzzyElem.addEventListener('keyup', () => { - validate(fuzzyElem) - }) - function toggleLinkedTextBox(checkBox, textBox){ - if(!checkBox.checked) { - textBox.classList.add('text-secondary') - textBox.disabled = true - } - else { - textBox.classList.remove('text-secondary') - textBox.disabled = false - } - } + diff --git a/views/partials/footer.ejs b/views/partials/footer.ejs index 8e39a33..c606278 100644 --- a/views/partials/footer.ejs +++ b/views/partials/footer.ejs @@ -10,10 +10,10 @@ + + + + \ No newline at end of file diff --git a/views/partials/header.ejs b/views/partials/header.ejs index ea2a82e..fd3713e 100644 --- a/views/partials/header.ejs +++ b/views/partials/header.ejs @@ -1,7 +1,3 @@ - - - -
- - - + \ No newline at end of file diff --git a/views/public/css/emulator.css b/views/public/css/emulator.css new file mode 100644 index 0000000..ccc71c3 --- /dev/null +++ b/views/public/css/emulator.css @@ -0,0 +1,137 @@ +/* Only keep basic container styling */ +#game-wrapper { + padding-top: 0; + background: #222; + border-radius: 8px; + overflow: hidden; + margin-bottom: 20px; + max-width: 1024px; + margin-left: auto; + margin-right: auto; +} + +/* Keep only the aspect ratio for the game container */ +#game { + aspect-ratio: 4/3; + max-height: 700px; +} + +/* Keep alert styling for consistency */ +.alert-secondary { + background-color: #2a2a2a; + border-color: #3a3a3a; + color: #999; + display: inline-block; + margin: 0 auto; + padding: 0.75rem 1.25rem; +} + +.alert-secondary a.alert-link { + color: #bbb; + text-decoration: underline; +} + +.alert-secondary a.alert-link:hover { + color: #fff; +} + +.progress-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: none; /* Hidden by default */ + justify-content: center; + align-items: center; + background-color: rgba(0, 0, 0, 0.8); + z-index: 1000; +} + +.progress { + box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); +} + +#progress-text { + font-size: 14px; + font-weight: bold; +} + +.alert-warning { + display: inline-block; + margin: 0 auto; + padding: 0.75rem 1.25rem; +} + +/* Security Warning Styles */ +.security-alert { + text-align: left; + border: none; + border-radius: 8px; + margin-bottom: 1rem; + padding: 0.75rem 1rem; + box-shadow: 0 2px 10px rgba(220, 53, 69, 0.3); + position: relative; + overflow: hidden; + font-size: 0.9rem; + display: inline-block; + line-height: 1.4; +} + +.security-alert::before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 4px; + height: 100%; + background: currentColor; + opacity: 0.7; +} + +.security-alert.alert-danger { + background-color: rgba(220, 53, 69, 0.1); + color: #dc3545; +} + +.security-alert.alert-warning { + background-color: rgba(255, 193, 7, 0.1); + color: #ffc107; +} + +.security-alert i { + vertical-align: middle; +} + +.security-alert strong { + vertical-align: middle; +} + +.security-alert ul { + margin-top: 0.75rem; + margin-bottom: 0; + padding-left: 2.5rem; + color: #e9ecef; +} + +.security-alert ul li { + margin: 0.5rem 0; + line-height: 1.4; +} + +.security-alert small { + display: block; + margin-top: 1rem; + padding-top: 1rem; + border-top: 1px solid rgba(255, 255, 255, 0.1); + color: #e9ecef; +} + +.security-alert pre { + background: rgba(0, 0, 0, 0.2); + border-radius: 4px; + padding: 0.75rem; + margin: 0.75rem 0 0; + color: #e9ecef; + font-size: 0.9rem; +} diff --git a/views/public/css/emulatorlist.css b/views/public/css/emulatorlist.css new file mode 100644 index 0000000..9d1cf10 --- /dev/null +++ b/views/public/css/emulatorlist.css @@ -0,0 +1,167 @@ +.emulators-container { + padding-bottom: 80px; +} + +.console-card { + transition: all 0.3s ease; + cursor: pointer; + overflow: hidden; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); +} + +.console-card:hover { + transform: translateY(-5px); + box-shadow: 0 8px 15px rgba(0, 0, 0, 0.3); + border-color: rgb(255, 189, 51); +} + +.console-icon { + max-height: 100px; + max-width: 90%; + object-fit: contain; + margin: 5px auto; + display: block; + transition: transform 0.2s; +} + +.console-card:hover .console-icon { + transform: scale(1.05); +} + +.console-card-title { + font-weight: bold; + margin-top: 10px; + padding: 12px; + background-color: rgba(0, 0, 0, 0.2); + border-radius: 0 0 4px 4px; + transition: background-color 0.3s; +} + +.console-card:hover .console-card-title { + background-color: rgba(255, 189, 51, 0.2); +} + +/* Modal styles */ +.modal-content { + border-radius: 8px; + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5); + border: 1px solid rgba(255, 189, 51, 0.2); +} + +.modal-header { + background-color: #1a1d21; + padding: 15px 20px; +} + +.modal-title { + font-weight: 600; + color: rgb(255, 189, 51); +} + +.modal-body { + padding: 25px; + max-height: 80vh; + overflow-y: auto; + background-color: #232629; +} + +/* Emulator card styles */ +.emulator-card { + background-color: #2a3030; + border: 1px solid rgba(255, 255, 255, 0.1); + margin-bottom: 20px; + border-radius: 8px; + transition: all 0.3s; + overflow: hidden; +} + +.emulator-card:hover { + transform: translateY(-3px); + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3); + border-color: rgba(255, 189, 51, 0.4); +} + +.emulator-header { + background-color: rgba(0, 0, 0, 0.2); + padding: 15px 20px; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); +} + +.emulator-header h5 { + margin: 0; + color: rgb(255, 189, 51); + font-weight: 600; +} + +.emulator-body { + padding: 20px; +} + +.emulator-logo-container { + display: flex; + justify-content: center; + align-items: center; + margin-bottom: 15px; + padding: 15px; + background-color: rgba(0, 0, 0, 0.1); + border-radius: 6px; + min-height: 120px; +} + +.emulator-logo { + max-height: 100px; + max-width: 100%; + object-fit: contain; + transition: transform 0.3s; +} + +.emulator-logo:hover { + transform: scale(1.05); +} + +.emulator-description { + margin-bottom: 20px; + line-height: 1.6; + color: #e0e0e0; +} + +.platform-badges { + display: flex; + flex-wrap: wrap; + margin-bottom: 20px; +} + +.platform-badge { + background-color: #3d4451; + border-radius: 20px; + padding: 5px 15px; + margin-right: 8px; + margin-bottom: 8px; + display: inline-block; + font-size: 0.85rem; + transition: all 0.2s; + border: 1px solid rgba(255, 255, 255, 0.1); +} + +.platform-badge:hover { + background-color: #4a5366; + transform: translateY(-2px); + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); +} + +.download-btn { + background-color: rgb(255, 189, 51); + color: #000; + border: none; + padding: 8px 20px; + border-radius: 5px; + font-weight: 600; + transition: all 0.3s; +} + +.download-btn:hover { + background-color: rgb(255, 210, 115); + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); + color: #000; +} diff --git a/views/public/css/error.css b/views/public/css/error.css new file mode 100644 index 0000000..39a8eec --- /dev/null +++ b/views/public/css/error.css @@ -0,0 +1,97 @@ +body { + background-color: #1a1a1a; + color: white; + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; +} +.error-container { + text-align: center; + padding: 2rem; + max-width: 800px; + width: 100%; + background-color: #262626; + border-radius: 12px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); +} +.error-code { + font-size: 8rem; + font-weight: bold; + color: #dc3545; + margin-bottom: 0.5rem; + text-shadow: 0 0 10px rgba(220, 53, 69, 0.5); +} +.error-message { + font-size: 2rem; + margin-bottom: 1.5rem; + color: #f8f9fa; +} +.error-details { + color: #adb5bd; + margin-bottom: 2rem; + padding: 1rem; + background-color: #333; + border-radius: 8px; + text-align: left; + max-height: 200px; + overflow-y: auto; + word-break: break-word; +} +.actions { + display: flex; + justify-content: center; + flex-wrap: wrap; + gap: 1rem; + margin-bottom: 1.5rem; +} +.error-info { + display: flex; + justify-content: space-between; + flex-wrap: wrap; + margin-top: 2rem; + font-size: 0.9rem; + color: #6c757d; + border-top: 1px solid #444; + padding-top: 1rem; +} +.error-info div { + margin: 0.5rem 1rem; +} +.btn { + padding: 0.6rem 1.5rem; + border-radius: 50px; + font-weight: 600; + transition: all 0.3s; +} +.btn-primary { + background-color: #007bff; + border-color: #007bff; +} +.btn-primary:hover { + background-color: #0069d9; + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3); +} +.btn-outline-secondary { + color: #f8f9fa; + border-color: #6c757d; +} +.btn-outline-secondary:hover { + background-color: #6c757d; + color: #f8f9fa; + transform: translateY(-2px); +} +.icon-large { + font-size: 1.5rem; + vertical-align: middle; +} +@media (max-width: 576px) { + .error-code { + font-size: 5rem; + } + .error-message { + font-size: 1.5rem; + } +} diff --git a/views/public/css/style.css b/views/public/css/style.css new file mode 100644 index 0000000..136009f --- /dev/null +++ b/views/public/css/style.css @@ -0,0 +1,146 @@ +html, +body { + height: 100%; +} +body { + padding: 0; + margin: 0; + background-color: #1c2020; + color: #fff !important; +} +a { + color: #ffffff; + text-decoration: none; +} +tr:hover td { + color: #ffffff; + background: #3d4351; +} +a:hover { + text-decoration: underline; + color: #ffffff; +} +td { + webkit-transition: background 300ms ease-in; + transition-behavior: normal; + transition-duration: 300ms; + transition-timing-function: ease-in; + transition-delay: 0s; + transition-property: background; + -moz-transition: background 300ms ease-in; + -ms-transition: background 300ms ease-in; + -o-transition: background 300ms ease-in; + transition: background 300ms ease-in; + transition-behavior: normal; + transition-duration: 300ms; + transition-timing-function: ease-in; + transition-delay: 0s; +} +td a { + display: block; +} + +.footer-text { + margin: 0; +} +.selected { + color: rgb(255, 189, 51) !important; +} +.hidden { + display: none; +} +.nav-link:hover, +.navbar-brand:hover { + color: #f0a400 !important; +} +.nav-link, +.navbar-brand { + transition: all 0.5s; +} +.card { + background-color: #262c2c; + border: 1px solid rgba(255, 255, 255, 0.325); +} +.form-control:focus { + border-color: rgb(255, 189, 51) !important; + box-shadow: 0 0 0 0.2rem rgba(240, 164, 0, 0.25) !important; +} +.form-control { + background-color: #343a40 !important; + color: #fff !important; +} +.page-link { + background-color: #343a40 !important; + color: rgb(255, 189, 51) !important; + transition: all 0.5s; +} +.page-item.active .page-link { + border-color: rgb(255, 189, 51) !important; +} +.page-link:hover { + color: #f0a400 !important; + border-color: #f0a400; +} +.page-item.disabled .page-link { + color: #6c757d !important; +} +.custom-select:focus { + box-shadow: 0 0 0 0.2rem rgba(240, 164, 0, 0.25) !important; +} +.stats { + display: inline-block; + color: #6c757d; + margin-bottom: 5px; +} +.SuggestionList { + text-align: left; + display: none; + list-style: none; + list-style-image: none; + padding: 0; + border: 1px solid #ccc; + margin: 0 0 0.2em 0; + border-radius: 3px; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); + background: #262c2c; + background-color: #262c2c; + background-image: none; + position: absolute; + z-index: 20; +} +.Suggestion { + padding: 0.5em 1em; + border-bottom: 1px solid #eee; +} +.Suggestion:last-child { + border: none; +} +.Suggestion:hover { + background: #484f60; +} +.Suggestion.selected { + background: #576075; +} + +.dropdown-item:hover { + background-color: #3d4351 !important; +} +.dropdown-item.active { + background-color: #576075 !important; +} +.btn-dark { + border-color: #576075; +} +.btn-dark:focus, +.btn-dark:hover { + border-color: rgb(255, 189, 51) !important; + box-shadow: 0 0 0 0.2rem rgba(240, 164, 0, 0.25) !important; +} +/* Ensure dropdown menu appears on top of everything */ +.dropdown-menu.show { + z-index: 9999 !important; +} +.dropdown { + position: relative; + z-index: 9999 !important; +} diff --git a/views/public/js/emulator.js b/views/public/js/emulator.js new file mode 100644 index 0000000..68a2f98 --- /dev/null +++ b/views/public/js/emulator.js @@ -0,0 +1,226 @@ +// Check if in a context that supports SharedArrayBuffer +const isHttps = window.location.protocol === "https:"; +const hasSharedArrayBuffer = typeof SharedArrayBuffer !== "undefined"; +const isCrossOriginIsolated = window.crossOriginIsolated === true; +const canUseThreads = hasSharedArrayBuffer && isCrossOriginIsolated; + +// Display security warnings +const warningsDiv = document.getElementById("security-warnings"); + +if (!isHttps) { + warningsDiv.innerHTML += ` +
+ + ${emuStrings.warning.https.split(":")[0]}: + ${emuStrings.warning.https.split(":")[1]} +
+ `; +} + +// Display important notice immediately +console.log( + "%cAbout this Page", + "font-size: 20px; font-weight: bold; color: #4CAF50;" +); +console.log( + `%c ${emuStrings.console.about} \n` + + `${emuStrings.console.disclaimer} \n` + + `${emuStrings.console.more_info}`, + "font-size: 14px; color: #90CAF9;" +); +console.log( + `%c${window.location.origin}/about`, + "font-size: 14px; color: #90CAF9;" +); + +// Configure EmulatorJS +console.log("[Emulator] Starting emulator configuration"); +console.log("[Emulator] System:", emulatorConfig.system); +console.log("[Emulator] Core:", emulatorConfig.core); + +console.log("[Emulator] SharedArrayBuffer available:", hasSharedArrayBuffer); +console.log("[Emulator] Cross-Origin-Isolation status:", isCrossOriginIsolated); +console.log("[Emulator] Can use threads:", canUseThreads); + +window.EJS_player = "#game"; + +window.EJS_core = emulatorConfig.core; +window.EJS_gameUrl = `/proxy-rom/${romFile.id}`; +window.EJS_pathtodata = "/emulatorjs/data/"; +window.EJS_startOnLoaded = true; +window.EJS_gameID = 1; + +// Using threads improves performance by a lot +// But also creates freezes, crashes and some emulators need to be reconfigured to work +// This should be revisited in the future. +// We're using threads only on PSP for now +window.EJS_threads = + emulatorConfig.system === "Sony PlayStation Portable" + ? navigator.hardwareConcurrency || 4 + : false; +window.EJS_gameName = romFile.filename.replace(/\.[^/.]+$/, ""); +window.EJS_backgroundBlur = true; +window.EJS_defaultOptions = { + "save-state-slot": 1, + "save-state-location": "local", +}; + +// BIOS configuration +window.EJS_biosUrl = emulatorConfig.bios + ? "/proxy-bios?url=" + + encodeURIComponent( + JSON.stringify(Object.values(emulatorConfig.bios.files)[0].url) + ) + : undefined; + +console.log("[Emulator] BIOS configuration:", window.EJS_biosUrl); + +// Required for Sega CD ?? +window.EJS_loadStateURL = window.location.href; +window.EJS_saveStateURL = window.location.href; +window.EJS_cheats = true; + +// Add error event listener for the emulator +window.EJS_onGameStart = () => { + console.log("[Emulator] Game started successfully"); +}; + +window.EJS_onLoadState = (state) => { + console.log("[Emulator] Load state:", state); +}; + +window.EJS_onSaveState = (state) => { + console.log("[Emulator] Save state:", state); +}; + +window.EJS_onLoadError = (error) => { + console.error("[Emulator] Load error:", error); +}; + +async function loadRom() { + try { + console.log("[Emulator] Starting ROM load process"); + const progressContainer = document.getElementById("progress-container"); + const progressBar = document.getElementById("download-progress"); + const progressText = document.getElementById("progress-text"); + progressContainer.style.display = "flex"; + const isCompressed = /\.(zip|7z)$/i.test(`${romFile.filename}`); + const shouldUnpack = emulatorConfig.unpackRoms; + console.log( + `[Emulator] ROM compression status: ${ + isCompressed ? "compressed" : "uncompressed" + }` + ); + console.log(`[Emulator] Should unpack: ${shouldUnpack}`); + + progressText.textContent = `${emuStrings.loading.downloading} (0%)`; + console.log("[Emulator] Initiating ROM download"); + + const response = await fetch(EJS_gameUrl); + if (!response.ok) { + throw new Error( + `${emuStrings.error.http_error}, { status: "response.status" })`.replace( + "response.status", + response.status + ) + ); + } + + // If we're not unpacking, still show download progress but return direct URL + if (!isCompressed || !shouldUnpack) { + const contentLength = response.headers.get("content-length"); + const total = parseInt(contentLength, 10); + let loaded = 0; + + const reader = response.body.getReader(); + const chunks = []; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + chunks.push(value); + loaded += value.length; + + const percent = Math.round((loaded / total) * 100); + progressBar.style.width = percent + "%"; + progressText.textContent = `${emuStrings.loading.downloading} (${percent}%)`; + } + + console.log("[Emulator] Using direct URL for ROM"); + progressContainer.style.display = "none"; + + // Create blob from chunks for direct loading + const blob = new Blob(chunks); + return URL.createObjectURL(blob); + } + + // For compressed files that need unpacking, continue with decompression + const contentLength = response.headers.get("content-length"); + const total = parseInt(contentLength, 10); + let loaded = 0; + + const reader = response.body.getReader(); + const chunks = []; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + chunks.push(value); + loaded += value.length; + + const percent = Math.round((loaded / total) * 100); + progressBar.style.width = percent + "%"; + progressText.textContent = `${emuStrings.loading.downloading} (${percent}%)`; + } + + // Decompression phase + progressText.textContent = `${emuStrings.loading.decompressing}`; + console.log("[Emulator] Starting ZIP extraction"); + + const blob = new Blob(chunks); + const zip = await JSZip.loadAsync(blob); + const files = Object.keys(zip.files); + console.log("[Emulator] ZIP contents:", files); + + const rom = files.find((f) => !zip.files[f].dir); + if (!rom) { + throw new Error(emuStrings.error.no_rom); + } + console.log("[Emulator] Found ROM file in ZIP:", romFile); + + const romData = await zip.files[rom].async("blob"); + console.log("[Emulator] ROM extraction complete"); + progressContainer.style.display = "none"; + return URL.createObjectURL(romData); + } catch (error) { + console.error("[Emulator] Error in loadRom:", error); + throw error; + } +} + +loadRom() + .then((romUrl) => { + console.log("[Emulator] ROM loaded successfully, initializing EmulatorJS"); + window.EJS_gameUrl = romUrl; + + // We need to wait a moment to ensure cross-origin isolation is properly applied + setTimeout(() => { + const script = document.createElement("script"); + script.src = `${window.EJS_pathtodata}loader.js`; + script.onerror = (error) => { + const gameDiv = document.getElementById("game"); + gameDiv.innerHTML = `
+ Failed to load EmulatorJS. Please refresh the page or try again later. +
`; + }; + document.body.appendChild(script); + }, 500); + }) + .catch((error) => { + const gameDiv = document.getElementById("game"); + gameDiv.innerHTML = `
+ ${emuStrings.error.loading}: ${error.message} +
`; + }); diff --git a/views/public/js/emulatorlist.js b/views/public/js/emulatorlist.js new file mode 100644 index 0000000..c759b84 --- /dev/null +++ b/views/public/js/emulatorlist.js @@ -0,0 +1,87 @@ +function showEmulators(consoleName) { + const modalTitle = document.getElementById("emulatorModalLabel"); + const emulatorList = document.getElementById("emulatorList"); + const consoleData = emulators[consoleName] + + modalTitle.innerHTML = `${recommended} | ${consoleName}`; + emulatorList.innerHTML = ""; + + // Create console icon at the top of the modal + const consoleIconContainer = document.createElement("div"); + consoleIconContainer.className = "text-center mb-4"; + consoleIconContainer.innerHTML = ` + ${consoleName} +

${consoleName}

+
+ `; + emulatorList.appendChild(consoleIconContainer); + + Object.entries(consoleData.emulators).forEach( + ([emulatorName, emulatorData]) => { + const emulatorCard = document.createElement("div"); + emulatorCard.className = "emulator-card"; + + let platformsHtml = ""; + emulatorData.platforms.forEach((platform) => { + let iconClass = ""; + switch (platform.toLowerCase()) { + case "windows": + iconClass = "fab fa-windows"; + break; + case "linux": + iconClass = "fab fa-linux"; + break; + case "macos": + iconClass = "fab fa-apple"; + break; + case "android": + iconClass = "fab fa-android"; + break; + case "ios": + iconClass = "fab fa-app-store-ios"; + break; + default: + iconClass = "fas fa-desktop"; + } + platformsHtml += `${platform}`; + }); + + emulatorCard.innerHTML = ` +
+
${emulatorName}
+
+
+
+
+
+ +
+
+
+
${ + emulatorData.description + }
+
+ ${platformsHtml} +
+ + ${download} + +
+
+
+ `; + + emulatorList.appendChild(emulatorCard); + } + ); + + // Show the modal + $("#emulatorModal").modal("show"); +} diff --git a/views/public/js/navbar.js b/views/public/js/navbar.js new file mode 100644 index 0000000..ff5ffdb --- /dev/null +++ b/views/public/js/navbar.js @@ -0,0 +1,19 @@ + $(document).ready(function() { + // Make sure Bootstrap dropdown is properly initialized + $('.dropdown-toggle').dropdown(); + }); + + const aTags = document.querySelectorAll('a') + aTags.forEach(aTag => { + if(aTag.getAttribute('href') == window.location.pathname){ + aTag.classList.add('selected') + aTag.classList.remove('hidden') + } + }) + + function changeLanguage(lang) { + // Create URL with new language parameter + const url = new URL(window.location.href); + url.searchParams.set('lang', lang); + window.location.href = url.toString(); + } \ No newline at end of file diff --git a/views/public/js/settings.js b/views/public/js/settings.js new file mode 100644 index 0000000..d1514d7 --- /dev/null +++ b/views/public/js/settings.js @@ -0,0 +1,103 @@ + function setBoosts(){ + for(let boost in defaults.boost){ + if(typeof settings.boost[boost] == 'undefined') {settings.boost[boost] == defaults.boost[boost]} + document.getElementById(boost + 'boost').value = settings.boost[boost] + document.getElementById(boost + 'boost').addEventListener('keyup', () => { + validate(document.getElementById(boost + 'boost')) + }) + } + } + + function setColumns(){ + for(let field in settings.fields){ + let element = document.getElementById(settings.fields[field]) + if(!element){settings.fields.splice(field, 1)} + else{element.checked = true} + } + for(let field in defaults.fields){ + document.getElementById(defaults.fields[field]).addEventListener('change', () => { + toggleLinkedTextBox(document.getElementById(defaults.fields[field]), document.getElementById(defaults.fields[field] + 'boost')) + }) + let elem = document.getElementById(defaults.fields[field]) + if(!elem.checked){ + let boostField = document.getElementById(defaults.fields[field] + 'boost') + boostField.classList.add('text-secondary') + boostField.disabled = true + } + } + } + + function setOthers(){ + if(typeof settings.combineWith == 'undefined') {settings.combineWith = defaults.combineWith} + if(typeof settings.fuzzy == 'undefined') {settings.fuzzy = defaults.fuzzy} + if(typeof settings.prefix == 'undefined') {settings.prefix = defaults.prefix} + if(typeof settings.hideNonGame == 'undefined') {settings.hideNonGame = defaults.hideNonGame} + document.getElementById('combineWith').checked = settings.combineWith ? true : false + document.getElementById('fuzzy').value = settings.fuzzy + document.getElementById('prefix').checked = settings.prefix + document.getElementById('hideNonGame').checked = settings.hideNonGame + } + + function saveSettings(){ + for(let boost in defaults.boost){settings.boost[boost] = parseInt(document.getElementById(boost + 'boost').value)} + settings.fields = [] + for(let field in defaults.fields){ + if(document.getElementById(defaults.fields[field]).checked){ + settings.fields.push(defaults.fields[field]) + } + } + settings.combineWith = document.getElementById('combineWith').checked ? 'AND' : '' + settings.fuzzy = parseFloat (document.getElementById('fuzzy').value) + settings.prefix = document.getElementById('prefix').checked + settings.hideNonGame = document.getElementById('hideNonGame').checked + localStorage.setItem('settings', JSON.stringify(settings)) + window.location.href = '/' + } + + function loadSettings(){ + $(function () { + $('[data-toggle="tooltip"]').tooltip() + }) + if(!settingStore) { + settings = structuredClone(defaults) + settingStore = JSON.stringify(settings) + localStorage.setItem('settings', settingStore) + } + else{ + try{ + settings = JSON.parse(settingStore) + } + catch{ + //load defaults if not exist + settings = defaults + } + } + setBoosts() + setColumns() + setOthers() + } + document.body.onload = loadSettings + document.getElementById('saveSettings').onclick = saveSettings + + function validate(element){ + let max = parseInt(element.max) + let min = parseInt(element.min) + let value = parseInt(element.value) + if(value > max) {element.value = max} + if(value < min) {element.value = min} + console.log(max, min, value) + } + fuzzyElem = document.getElementById('fuzzy') + fuzzyElem.addEventListener('keyup', () => { + validate(fuzzyElem) + }) + function toggleLinkedTextBox(checkBox, textBox){ + if(!checkBox.checked) { + textBox.classList.add('text-secondary') + textBox.disabled = true + } + else { + textBox.classList.remove('text-secondary') + textBox.disabled = false + } + } \ No newline at end of file diff --git a/views/public/js/suggestions.js b/views/public/js/suggestions.js new file mode 100644 index 0000000..3966802 --- /dev/null +++ b/views/public/js/suggestions.js @@ -0,0 +1,154 @@ +typingTimeout = null; +selectedSuggestion = null; +totalSuggestions = 0; +async function getSuggestions(query) { + await fetch("/suggest", { + method: "POST", + body: JSON.stringify({ query: query }), + headers: { "Content-type": "application/json; charset=UTF-8" }, + }) + .then((response) => response.json()) + .then((json) => populateSuggestions(json)); +} +async function populateSuggestions(suggestArr) { + selectedSuggestion = null; + suggestions = suggestArr.suggestions; + let suggestionList = document.getElementById("suggestionList"); + suggestionList.replaceChildren(); + let searchElem = document.getElementById("search"); + let listLength = suggestions.length > 10 ? 10 : suggestions.length; + totalSuggestions = listLength; + for (let x = 0; x < listLength; x++) { + let listElem = document.createElement("li"); + listElem.classList.add("Suggestion"); + listElem.innerText = suggestions[x].suggestion; + listElem.addEventListener("click", (e) => { + searchElem.value = listElem.innerText; + suggestionList.style.display = "none"; + selectedSuggestion = null; + totalSuggestions = 0; + }); + listElem.addEventListener("mouseover", (e) => { + selectedSuggestion = null; + clearSelects(); + }); + listElem.id = `suggestions${x}`; + suggestionList.appendChild(listElem); + suggestionList.style.display = "block"; + } +} +document.addEventListener( + "DOMContentLoaded", + function (e) { + searchElem = document.getElementById("search"); + if (!searchElem) { + return; + } + searchElem / + addEventListener("keydown", function (e) { + if (e.key === "Enter") { + if (selectedSuggestion != null) { + e.preventDefault(); + } + } + if (e.key === "ArrowUp" || e.key === "ArrowDown") { + e.preventDefault(); + } + }); + searchElem.addEventListener("keyup", function (e) { + if (e.key === "Escape") { + return; + } + if (e.key === "ArrowUp") { + if (!totalSuggestions) return; + if (typeof selectedSuggestion != "number") { + selectedSuggestion = totalSuggestions - 1; + } else { + selectedSuggestion -= 1; + if (selectedSuggestion < 0) { + selectedSuggestion = totalSuggestions - 1; + } + } + selectSuggestion(selectedSuggestion); + return; + } + if (e.key === "ArrowDown") { + if (!totalSuggestions) return; + if (typeof selectedSuggestion != "number") { + selectedSuggestion = 0; + } else { + selectedSuggestion += 1; + if (selectedSuggestion > totalSuggestions - 1) { + selectedSuggestion = 0; + } + } + selectSuggestion(selectedSuggestion); + return; + } + if (e.key === "Enter") { + if (selectedSuggestion != null) { + enterSuggestion(selectedSuggestion); + return; + } + return; + } + query = searchElem.value; + if (typingTimeout != null) { + clearTimeout(typingTimeout); + } + typingTimeout = setTimeout(function () { + typingTimeout = null; + if (!query) { + let suggestionList = document.getElementById("suggestionList"); + suggestionList.replaceChildren(); + suggestionList.style.display = "none"; + totalSuggestions = 0; + } else { + getSuggestions(query); + } + }, 500); + }); + + document.body.addEventListener("click", (e) => { + let suggestionList = document.getElementById("suggestionList"); + suggestionList.style.display = "none"; + totalSuggestions = 0; + }); + document.addEventListener("keyup", (e) => { + if (e.key === "Escape") { + let suggestionList = document.getElementById("suggestionList"); + suggestionList.style.display = "none"; + totalSuggestions = 0; + } + }); + }, + false +); +function selectSuggestion(id) { + let suggestId = `suggestions${id}`; + clearSelects(); + document.getElementById(suggestId).classList.add("selected"); +} +function enterSuggestion(id) { + let suggestId = `suggestions${id}`; + clearSelects(); + document.getElementById("search").value = + document.getElementById(suggestId).innerText; + selectedSuggestion = null; + suggestionList.style.display = "none"; + totalSuggestions = 0; +} +function clearSelects() { + let suggestionList = document.getElementById("suggestionList"); + let selectedItems = suggestionList.getElementsByClassName("selected"); + if (!selectedItems.length) { + return; + } + for (item in selectedItems) { + if (typeof selectedItems[item].classList === "undefined") { + //this is jank but the stupid function double fires + return; + } + selectedItems[item].classList.remove("selected"); + } +}