mirror of
https://github.com/alexankitty/Myrient-Search-Engine.git
synced 2026-01-15 16:33:15 -03:00
Merge pull request #24 from alexankitty/resource-loader
Implements static resource loading and separates out ejs files (nostly)
This commit is contained in:
@@ -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>
|
||||
@@ -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>
|
||||
<% } %>
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)}`
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
137
views/public/css/emulator.css
Normal file
137
views/public/css/emulator.css
Normal 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;
|
||||
}
|
||||
167
views/public/css/emulatorlist.css
Normal file
167
views/public/css/emulatorlist.css
Normal 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;
|
||||
}
|
||||
97
views/public/css/error.css
Normal file
97
views/public/css/error.css
Normal 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
146
views/public/css/style.css
Normal 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
226
views/public/js/emulator.js
Normal 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>`;
|
||||
});
|
||||
87
views/public/js/emulatorlist.js
Normal file
87
views/public/js/emulatorlist.js
Normal 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
19
views/public/js/navbar.js
Normal 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
103
views/public/js/settings.js
Normal 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
|
||||
}
|
||||
}
|
||||
154
views/public/js/suggestions.js
Normal file
154
views/public/js/suggestions.js
Normal 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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user