Merge pull request #24 from alexankitty/resource-loader

Implements static resource loading and separates out ejs files (nostly)
This commit is contained in:
Alexandra
2025-05-21 18:05:29 -06:00
committed by GitHub
16 changed files with 1181 additions and 1121 deletions

View File

@@ -51,360 +51,36 @@
</div>
</div>
<style>
/* 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;
}
</style>
<link rel="stylesheet" href="/public/css/emulator.css">
<script src="https://code.jquery.com/jquery-3.7.1.min.js" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.5.2/js/bootstrap.min.js" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/js/all.min.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js"></script>
<script>
// 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;
const emulatorConfig = <%- JSON.stringify(emulatorConfig) %>
const romFile = <%- JSON.stringify(romFile) %>
// Display security warnings
const warningsDiv = document.getElementById('security-warnings');
if (!isHttps) {
warningsDiv.innerHTML += `
<div class="alert security-alert alert-danger py-2">
<i class="fas fa-exclamation-triangle mr-2"></i>
<strong><%= __('emulator.warning.https').split(':')[0] %>:</strong>
<%- __('emulator.warning.https').split(':')[1] %>
</div>
`;
}
// Display important notice immediately
console.log('%cAbout this Page', 'font-size: 20px; font-weight: bold; color: #4CAF50;');
console.log(
'%c<%= __("emulator.console.about") %>\n' +
'<%= __("emulator.console.disclaimer") %>\n' +
'<%= __("emulator.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 = <% if (emulatorConfig.bios) { %>
'/proxy-bios?url=' + encodeURIComponent(<%- JSON.stringify(Object.values(emulatorConfig.bios.files)[0].url) %>)
<% } else { %>
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 = '<%= __("emulator.loading.downloading") %> (0%)';
console.log('[Emulator] Initiating ROM download');
const response = await fetch(EJS_gameUrl);
if (!response.ok) {
throw new Error('<%= __("emulator.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 = `<%= __("emulator.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 = `<%= __("emulator.loading.downloading") %> (${percent}%)`;
}
// Decompression phase
progressText.textContent = '<%= __("emulator.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 romFile = files.find(f => !zip.files[f].dir);
if (!romFile) {
throw new Error('<%= __("emulator.error.no_rom") %>');
}
console.log('[Emulator] Found ROM file in ZIP:', romFile);
const romData = await zip.files[romFile].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;
const emuStrings = {
console: {
about: "<%= __("emulator.console.about") %>",
disclaimer: "<%= __("emulator.console.disclaimer") %>",
more_info: "<%= __("emulator.console.more_info") %>"
},
warning: {
https: "<%= __("emulator.warning.https") %>"
},
loading: {
downloading: "<%= __("emulator.loading.downloading") %>",
decompressing: "<%= __("emulator.loading.decompressing") %>"
},
error: {
http_error: "<%= __("emulator.error.http_error") %>",
no_rom: "<%= __("emulator.error.no_rom") %>",
loading: "<%= __("emulator.error.loading") %>"
}
}
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 = `<div class="alert alert-danger">
Failed to load EmulatorJS. Please refresh the page or try again later.
</div>`;
};
document.body.appendChild(script);
}, 500);
})
.catch(error => {
const gameDiv = document.getElementById('game');
gameDiv.innerHTML = `<div class="alert alert-danger">
<%= __("emulator.error.loading") %>: ${error.message}
</div>`;
});
</script>
<script src="/public/js/emulator.js" defer></script>
</body>
</html>

View File

@@ -2,6 +2,7 @@
<script src='https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.5.2/js/bootstrap.min.js'></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css">
<link rel="stylesheet" href="/public/css/emulatorlist.css">
<div class="emulators-container">
<pre style="font: 20px / 19px monospace; color: white; text-align: center; overflow: hidden;">
@@ -52,255 +53,23 @@
</div>
</div>
<style>
.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,.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;
}
</style>
<script>
const recommended = "<%= __('emulator.recommended') %>"
const download = "<%= __('emulator.download') %>"
const emulators = <%- JSON.stringify(emulators) %>
document.addEventListener('DOMContentLoaded', function() {
// Add click handlers to all console cards
// Add click handlers to all console cards
const consoleCards = document.querySelectorAll('.console-card');
consoleCards.forEach(card => {
card.addEventListener('click', function() {
const consoleName = this.getAttribute('data-console');
<% if (typeof emulators !== 'undefined' && emulators) { %>
showEmulators(consoleName);
<% } %>
});
});
});
function showEmulators(consoleName) {
<% if (typeof emulators !== 'undefined' && emulators) { %>
const consoleData = <%- JSON.stringify(emulators) %>[consoleName];
const modalTitle = document.getElementById('emulatorModalLabel');
const emulatorList = document.getElementById('emulatorList');
modalTitle.innerHTML = `<i class="fas fa-gamepad mr-2 text-warning"></i><%= __('emulator.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 = `
<img src="/proxy-image?url=${encodeURIComponent(consoleData.icon)}" alt="${consoleName}" style="max-height: 80px;">
<h4 class="mt-3 text-warning">${consoleName}</h4>
<div class="separator my-3"><span></span></div>
`;
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 += `<span class="platform-badge"><i class="${iconClass} mr-2"></i>${platform}</span>`;
});
emulatorCard.innerHTML = `
<div class="emulator-header">
<h5>${emulatorName}</h5>
</div>
<div class="emulator-body">
<div class="row">
<div class="col-md-4">
<div class="emulator-logo-container">
<img src="/proxy-image?url=${encodeURIComponent(emulatorData.logo)}" alt="${emulatorName}" class="emulator-logo">
</div>
</div>
<div class="col-md-8">
<div class="emulator-description">${emulatorData.description}</div>
<div class="platform-badges">
${platformsHtml}
</div>
<a href="${emulatorData.url}" target="_blank" class="btn download-btn">
<i class="fas fa-download mr-2"></i><%= __('emulator.download') %>
</a>
</div>
</div>
</div>
`;
emulatorList.appendChild(emulatorCard);
});
// Show the modal
$('#emulatorModal').modal('show');
<% } %>
}
</script>
</script>
<% if (typeof emulators !== 'undefined' && emulators) { %>
<script src="/public/js/emulatorlist.js"></script>
<% } %>

View File

@@ -6,105 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css">
<style>
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;
}
}
</style>
<link rel="stylesheet" href="/public/css/error.css">
</head>
<body>
<div class="error-container">

View File

@@ -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
}
}
</script>
<script src="public/js/settings.js"></script>

View File

@@ -10,10 +10,10 @@
<script defer>
function timeConverter(UNIX_timestamp){
var timestamp = parseInt(UNIX_timestamp)
var date = new Date(timestamp);
var options = { hour12: false };
return date.toLocaleString(options)
var timestamp = parseInt(UNIX_timestamp)
var date = new Date(timestamp);
var options = { hour12: false };
return date.toLocaleString(options)
}
document.getElementById('crawl-time').innerText += ` ${timeConverter('<%= crawlTime %>')}`
document.getElementById('file-count').innerText += ` ${(<%= fileCount %>).toLocaleString(undefined)}`

View File

@@ -6,283 +6,11 @@
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.5.2/css/bootstrap.min.css" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css" crossorigin="anonymous">
<link rel="icon" type="image/x-icon" href="public/images/favicon.png">
<style>
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;
}
<link rel="stylesheet" href="/public/css/style.css">
<link rel="icon" type="image/x-icon" href="/public/images/favicon.png">
.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,.325)
}
.form-control:focus {
border-color: rgb(255, 189, 51)!important;
box-shadow: 0 0 0 .2rem rgba(240, 164, 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 .2rem rgba(240, 164, 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;
}
</style>
<script defer>
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')
}
}
</script>
<script src="/public/js/suggestions.js" defer></script>
<script src="https://code.jquery.com/jquery-3.7.1.min.js" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.5.2/js/bootstrap.min.js" crossorigin="anonymous"></script>

View File

@@ -1,7 +1,3 @@
<script src="https://code.jquery.com/jquery-3.7.1.min.js" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.5.2/js/bootstrap.min.js" crossorigin="anonymous"></script>
<nav class="navbar navbar-expand-sm navbar-light text-white">
<a id="brand-name" class="navbar-brand text-white" href="/"><%= process.env.INSTANCE_NAME || 'Myrient' %> Search</a>
<a id="brand-name" class="navbar-brand text-white hidden" href="/search"><%= __('nav.results') %></a>
@@ -33,48 +29,4 @@
</div>
</nav>
<script>
$(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();
}
</script>
<style>
.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 .2rem rgba(240, 164, 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;
}
</style>
<script src="/public/js/navbar.js"></script>

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}
}

146
views/public/css/style.css Normal file
View File

@@ -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;
}

226
views/public/js/emulator.js Normal file
View File

@@ -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 += `
<div class="alert security-alert alert-danger py-2">
<i class="fas fa-exclamation-triangle mr-2"></i>
<strong>${emuStrings.warning.https.split(":")[0]}:</strong>
${emuStrings.warning.https.split(":")[1]}
</div>
`;
}
// 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 = `<div class="alert alert-danger">
Failed to load EmulatorJS. Please refresh the page or try again later.
</div>`;
};
document.body.appendChild(script);
}, 500);
})
.catch((error) => {
const gameDiv = document.getElementById("game");
gameDiv.innerHTML = `<div class="alert alert-danger">
${emuStrings.error.loading}: ${error.message}
</div>`;
});

View File

@@ -0,0 +1,87 @@
function showEmulators(consoleName) {
const modalTitle = document.getElementById("emulatorModalLabel");
const emulatorList = document.getElementById("emulatorList");
const consoleData = emulators[consoleName]
modalTitle.innerHTML = `<i class="fas fa-gamepad mr-2 text-warning"></i>${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 = `
<img src="/proxy-image?url=${encodeURIComponent(
consoleData.icon
)}" alt="${consoleName}" style="max-height: 80px;">
<h4 class="mt-3 text-warning">${consoleName}</h4>
<div class="separator my-3"><span></span></div>
`;
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 += `<span class="platform-badge"><i class="${iconClass} mr-2"></i>${platform}</span>`;
});
emulatorCard.innerHTML = `
<div class="emulator-header">
<h5>${emulatorName}</h5>
</div>
<div class="emulator-body">
<div class="row">
<div class="col-md-4">
<div class="emulator-logo-container">
<img src="/proxy-image?url=${encodeURIComponent(
emulatorData.logo
)}" alt="${emulatorName}" class="emulator-logo">
</div>
</div>
<div class="col-md-8">
<div class="emulator-description">${
emulatorData.description
}</div>
<div class="platform-badges">
${platformsHtml}
</div>
<a href="${
emulatorData.url
}" target="_blank" class="btn download-btn">
<i class="fas fa-download mr-2"></i>${download}
</a>
</div>
</div>
</div>
`;
emulatorList.appendChild(emulatorCard);
}
);
// Show the modal
$("#emulatorModal").modal("show");
}

19
views/public/js/navbar.js Normal file
View File

@@ -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();
}

103
views/public/js/settings.js Normal file
View File

@@ -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
}
}

View File

@@ -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");
}
}