mirror of
https://github.com/alexankitty/Myrient-Search-Engine.git
synced 2026-01-15 16:33:15 -03:00
- i18n localization
- Add PSP support on web emu - properly doing CORS (blame PPSSPP) - added contributors image on about - proxying emulatorjs to avoid internet blocking - initial seeds for multithreading on webemu - added new searchalikes - Added new error page
This commit is contained in:
@@ -2,54 +2,57 @@
|
||||
<div class="col-sm-12 my-auto text-center">
|
||||
<pre style="font: 20px / 19px monospace; color: white; text-align: center; overflow: hidden;">
|
||||
<%= generateAsciiArt() %>
|
||||
About
|
||||
<%= __('about.title') %>
|
||||
</pre>
|
||||
<div class="card w-auto mx-auto text-center d-inline-block p-3">
|
||||
<div class="about-content">
|
||||
<h4>About <%= process.env.INSTANCE_NAME || 'Myrient' %> Search</h4>
|
||||
<p>A search engine for <a href="https://github.com/alexankitty/myrient-global-search">Myrient</a> -
|
||||
a service by Erista dedicated to video game preservation.</p>
|
||||
<p>Myrient offers organized and publicly available video game collections, keeping them from becoming
|
||||
lost to time.</p>
|
||||
<p class="text-secondary mb-4">Not affiliated with Myrient/Erista!</p>
|
||||
<h4><%= __('about.title') %> <%= process.env.INSTANCE_NAME || 'Myrient' %> Search</h4>
|
||||
<p><%= __('app.description') %></p>
|
||||
<p><%= __('app.tagline') %></p>
|
||||
<p class="text-secondary mb-4"><%= __('app.disclaimer') %></p>
|
||||
|
||||
<div class="mb-4">
|
||||
<p>If you like this project, please consider supporting Myrient:</p>
|
||||
<a href="https://myrient.erista.me/donate/" class="btn btn-secondary">Donate to Myrient</a>
|
||||
<p><%= __('about.support') %></p>
|
||||
<a href="https://myrient.erista.me/donate/" class="btn btn-secondary"><%= __('about.donate') %></a>
|
||||
</div>
|
||||
|
||||
<div class="mb-4 border-top pt-3">
|
||||
<h5>Built-in Emulator</h5>
|
||||
<h5><%= __('about.emulator.title') %></h5>
|
||||
<% if (process.env.EMULATOR_ENABLED === 'true') { %>
|
||||
<p>This website includes a built-in emulator powered by <a href="https://emulatorjs.org/">EmulatorJS</a> that brings retro gaming directly to your browser.</p>
|
||||
<p>Compatible games will feature a play button on their search result page.</p>
|
||||
<p><%= __('about.emulator.description') %></p>
|
||||
<p><%= __('about.emulator.compatibility') %></p>
|
||||
|
||||
<p>For the best gaming experience, use a Chromium-based browser with hardware acceleration turned on.</p>
|
||||
<p><%= __('about.emulator.browser_tip') %></p>
|
||||
|
||||
<p class="text-secondary">
|
||||
<small>
|
||||
<i class="fas fa-info-circle"></i>
|
||||
Games are loaded directly from Myrient's public archive. Save states are stored locally in the browser.
|
||||
<%= __('about.emulator.save_states') %>
|
||||
</small>
|
||||
<br>
|
||||
<small>
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
ROM hacks, soundtracks, and other non-game content are not supported by the emulator and may fail to load.
|
||||
<%= __('about.emulator.limitations') %>
|
||||
</small>
|
||||
</p>
|
||||
|
||||
<% } else { %>
|
||||
<p>Web Emulator functionality was disabled by the administrator.</p>
|
||||
<p>Contact the administrator or spin up your own instance of <%= process.env.INSTANCE_NAME || 'Myrient' %> Search.</p>
|
||||
<p><%= __('about.emulator.disabled') %></p>
|
||||
<p><%= __('about.emulator.contact') %></p>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<div class="border-top pt-3">
|
||||
<p>Search engine created by <a href="https://github.com/alexankitty">Alexankitty</a></p>
|
||||
<p><a href="https://github.com/alexankitty/myrient-global-search">View project on GitHub</a></p>
|
||||
<p><%= __('about.credits.created_by') %> <a href="https://github.com/alexankitty">Alexankitty</a></p>
|
||||
<div class="mb-3">
|
||||
<a href="https://github.com/alexankitty/Myrient-Search-Engine/graphs/contributors">
|
||||
<img src="/proxy-image?url=<%= encodeURIComponent('https://contrib.rocks/image?repo=alexankitty/Myrient-Search-Engine') %>" alt="Contributors" />
|
||||
</a>
|
||||
</div>
|
||||
<p><a href="https://github.com/alexankitty/myrient-global-search"><%= __('about.credits.view_github') %></a></p>
|
||||
<a href='https://ko-fi.com/Q5Q4IFNAO' target='_blank'>
|
||||
<img height='36' style='border:0px;height:36px;'
|
||||
src='https://storage.ko-fi.com/cdn/kofi5.png?v=3' alt='Buy Me a Coffee at ko-fi.com' />
|
||||
src='/proxy-image?url=<%= encodeURIComponent("https://storage.ko-fi.com/cdn/kofi5.png?v=3") %>' alt='Buy Me a Coffee at ko-fi.com' />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,292 +1,410 @@
|
||||
<div class="container-fluid">
|
||||
<!-- Header with game info -->
|
||||
<div class="row mb-4 mt-3">
|
||||
<div class="col-12 text-center">
|
||||
<h2 class="text-white"><%= romFile.filename.replace(/\.[^/.]+$/, '') %></h2>
|
||||
<p class="text-secondary"><%= romFile.category %></p>
|
||||
<div class="container-fluid">
|
||||
<!-- Header with game info -->
|
||||
<div class="row mb-4 mt-3">
|
||||
<div class="col-12 text-center">
|
||||
<h2 class="text-white"><%= romFile.filename.replace(/\.[^/.]+$/, '') %></h2>
|
||||
<p class="text-secondary"><%= romFile.category %></p>
|
||||
|
||||
<% if (isNonGame) { %>
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
Warning: This file may not be a game ROM and might not work properly in the web emulator.
|
||||
See the <a href="/about" class="alert-link">About</a> page for more information.
|
||||
<% if (isNonGame) { %>
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
<%= __('emulator.warning.non_game') %>
|
||||
<%= __('emulator.warning.see_about', { link: __('nav.about') }) %>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<!-- Security and Compatibility Warnings -->
|
||||
<div id="security-warnings"></div>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main game container with proper padding and height -->
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12 col-lg-10 col-xl-8">
|
||||
<div id="game-wrapper" class="position-relative">
|
||||
<div id="game" class="w-100"></div>
|
||||
<!-- Progress bar inside game container -->
|
||||
<div id="progress-container" class="progress-overlay">
|
||||
<div class="progress" style="height: 25px; background-color: #2a2a2a; width: 80%; max-width: 500px;">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated"
|
||||
role="progressbar"
|
||||
style="width: 0%"
|
||||
id="download-progress">
|
||||
<span id="progress-text">Loading ROM...</span>
|
||||
<!-- Main game container with proper padding and height -->
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12 col-lg-10 col-xl-8">
|
||||
<div id="game-wrapper" class="position-relative">
|
||||
<div id="game" class="w-100"></div>
|
||||
<!-- Progress bar inside game container -->
|
||||
<div id="progress-container" class="progress-overlay">
|
||||
<div class="progress" style="height: 25px; background-color: #2a2a2a; width: 80%; max-width: 500px;">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated"
|
||||
role="progressbar"
|
||||
style="width: 0%"
|
||||
id="download-progress">
|
||||
<span id="progress-text"><%= __('emulator.loading.rom') %></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Disclaimer footer -->
|
||||
<div class="row mt-4 mb-3">
|
||||
<div class="col-12 text-center">
|
||||
<div class="alert alert-secondary" role="alert">
|
||||
<small>
|
||||
<i class="fas fa-info-circle"></i>
|
||||
This emulator loads ROMs directly from <a href="https://myrient.erista.me/" class="alert-link">Myrient</a> and is not affiliated with them.
|
||||
See the <a href="/about" class="alert-link">About</a> page for more information.
|
||||
</small>
|
||||
<!-- Disclaimer footer -->
|
||||
<div class="row mt-4 mb-3">
|
||||
<div class="col-12 text-center">
|
||||
<div class="alert alert-secondary" role="alert">
|
||||
<small>
|
||||
<i class="fas fa-info-circle"></i>
|
||||
<%= __('emulator.disclaimer', { link: 'Myrient', about: __('nav.about') }) %>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js"></script>
|
||||
<script>
|
||||
// Display important notice immediately
|
||||
console.log('%cAbout this Page', 'font-size: 20px; font-weight: bold; color: #4CAF50;');
|
||||
console.log(
|
||||
'%cThis page contains a game emulator that provides access to games through Myrient\'s public archive.\n' +
|
||||
'We are not affiliated with or endorsed by Myrient.\n' +
|
||||
'Visit the About page for more details on how this integration works and other important information.',
|
||||
'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 %>');
|
||||
|
||||
window.EJS_player = '#game';
|
||||
window.EJS_core = '<%= emulatorConfig.core %>';
|
||||
window.EJS_gameUrl = '/proxy-rom/<%= romFile.id %>';
|
||||
window.EJS_pathtodata = 'https://cdn.emulatorjs.org/stable/data/';
|
||||
window.EJS_startOnLoaded = true;
|
||||
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 = 'Downloading ROM...';
|
||||
console.log('[Emulator] Initiating ROM download');
|
||||
|
||||
const response = await fetch(EJS_gameUrl);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
<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;
|
||||
}
|
||||
|
||||
// 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 = `Downloading ROM: ${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);
|
||||
/* Keep only the aspect ratio for the game container */
|
||||
#game {
|
||||
aspect-ratio: 4/3;
|
||||
max-height: 700px;
|
||||
}
|
||||
|
||||
// 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 = `Downloading ROM: ${percent}%`;
|
||||
/* 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;
|
||||
}
|
||||
|
||||
// Decompression phase
|
||||
progressText.textContent = 'Decompressing the game...';
|
||||
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('No ROM file found in ZIP archive');
|
||||
.alert-secondary a.alert-link {
|
||||
color: #bbb;
|
||||
text-decoration: underline;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
.alert-secondary a.alert-link:hover {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
loadRom()
|
||||
.then(romUrl => {
|
||||
console.log('[Emulator] ROM loaded successfully, initializing EmulatorJS');
|
||||
window.EJS_gameUrl = romUrl;
|
||||
const script = document.createElement('script');
|
||||
script.src = `${window.EJS_pathtodata}loader.js`;
|
||||
script.onerror = (error) => {
|
||||
console.error('[Emulator] Failed to load EmulatorJS:', error);
|
||||
.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>
|
||||
|
||||
<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;
|
||||
|
||||
// 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'
|
||||
};
|
||||
document.body.appendChild(script);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('[Emulator] Fatal error:', error);
|
||||
const gameDiv = document.getElementById('game');
|
||||
gameDiv.innerHTML = `<div class="alert alert-danger">
|
||||
Error loading game: ${error.message}
|
||||
</div>`;
|
||||
});
|
||||
</script>
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
</body>
|
||||
</html>
|
||||
@@ -6,7 +6,7 @@
|
||||
<div class="emulators-container">
|
||||
<pre style="font: 20px / 19px monospace; color: white; text-align: center; overflow: hidden;">
|
||||
<%= generateAsciiArt() %>
|
||||
Emulators
|
||||
<%= __('nav.emulators') %>
|
||||
</pre>
|
||||
|
||||
<div class="container mt-4">
|
||||
@@ -16,7 +16,7 @@
|
||||
<div class="col-md-4 col-sm-6 mb-4">
|
||||
<div class="card console-card h-100" data-console="<%= consoleName %>">
|
||||
<div class="text-center pt-3">
|
||||
<img src="<%= consoleData.icon %>" alt="<%= consoleName %>" class="console-icon mb-2">
|
||||
<img src="/proxy-image?url=<%= encodeURIComponent(consoleData.icon) %>" alt="<%= consoleName %>" class="console-icon mb-2">
|
||||
<div class="console-card-title"><%= consoleName %></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -25,7 +25,7 @@
|
||||
<% } else { %>
|
||||
<div class="col-12 text-center">
|
||||
<div class="alert alert-warning">
|
||||
No emulator data available. Please check your configuration.
|
||||
<%= __('emulator.warning.no_data') %>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
@@ -39,7 +39,7 @@
|
||||
<div class="modal-content bg-dark text-white">
|
||||
<div class="modal-header border-bottom border-secondary">
|
||||
<h5 class="modal-title" id="emulatorModalLabel">
|
||||
<i class="fas fa-gamepad mr-2 text-warning"></i>Recommended Emulators
|
||||
<i class="fas fa-gamepad mr-2 text-warning"></i><%= __('emulator.recommended') %>
|
||||
</h5>
|
||||
<button type="button" class="close text-white" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
@@ -241,14 +241,14 @@
|
||||
const modalTitle = document.getElementById('emulatorModalLabel');
|
||||
const emulatorList = document.getElementById('emulatorList');
|
||||
|
||||
modalTitle.innerHTML = `<i class="fas fa-gamepad mr-2 text-warning"></i>Recommended Emulators | ${consoleName}`;
|
||||
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="${consoleData.icon}" alt="${consoleName}" style="max-height: 80px;">
|
||||
<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>
|
||||
`;
|
||||
@@ -280,7 +280,7 @@
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="emulator-logo-container">
|
||||
<img src="${emulatorData.logo}" alt="${emulatorName}" class="emulator-logo">
|
||||
<img src="/proxy-image?url=${encodeURIComponent(emulatorData.logo)}" alt="${emulatorName}" class="emulator-logo">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
@@ -289,7 +289,7 @@
|
||||
${platformsHtml}
|
||||
</div>
|
||||
<a href="${emulatorData.url}" target="_blank" class="btn download-btn">
|
||||
<i class="fas fa-download mr-2"></i>Download
|
||||
<i class="fas fa-download mr-2"></i><%= __('emulator.download') %>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
149
views/pages/error.ejs
Normal file
149
views/pages/error.ejs
Normal file
@@ -0,0 +1,149 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title><%= __('error.title') %> - <%= process.env.INSTANCE_NAME || 'Myrient' %></title>
|
||||
<meta charset="utf-8">
|
||||
<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>
|
||||
</head>
|
||||
<body>
|
||||
<div class="error-container">
|
||||
<div class="error-code"><%= status %></div>
|
||||
<div class="error-message">
|
||||
<%= __('error.message') %>
|
||||
</div>
|
||||
<% if (message) { %>
|
||||
<div class="error-details">
|
||||
<code><%= __('error.details', { message: message }) %></code>
|
||||
<% if (stack && process.env.NODE_ENV !== 'production') { %>
|
||||
<pre class="mt-2"><%= stack %></pre>
|
||||
<% } %>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<div class="actions">
|
||||
<a href="/" class="btn btn-primary">
|
||||
<i class="bi bi-house-fill mr-2"></i> <%= __('error.back_home') %>
|
||||
</a>
|
||||
<button onclick="window.history.back()" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left mr-2"></i> <%= __('error.go_back') %>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="error-info">
|
||||
<div>
|
||||
<i class="bi bi-clock icon-large"></i>
|
||||
<span id="timestamp"><%= new Date().toISOString() %></span>
|
||||
</div>
|
||||
<div>
|
||||
<i class="bi bi-bookmark icon-large"></i>
|
||||
<span><%= req.originalUrl || 'Unknown URL' %></span>
|
||||
</div>
|
||||
<div>
|
||||
<i class="bi bi-shield-exclamation icon-large"></i>
|
||||
<span>Request ID: <%= requestId %></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -8,12 +8,12 @@
|
||||
let entryEnd = entryStart + 100
|
||||
entryEnd = entryEnd > results.items.length ? results.items.length : entryEnd
|
||||
%>
|
||||
<script src='https://code.jquery.com/jquery-3.7.1.js'></script>
|
||||
<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>
|
||||
<script src='https://cdn.datatables.net/2.1.8/js/dataTables.js'></script>
|
||||
<script src='https://cdn.datatables.net/2.1.8/js/dataTables.bootstrap4.js'></script>
|
||||
<link rel="stylesheet" href="https://cdn.datatables.net/2.1.8/css/dataTables.bootstrap4.css">
|
||||
<script src='https://code.jquery.com/jquery-3.7.1.js' crossorigin="anonymous"></script>
|
||||
<script src='https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/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://cdn.datatables.net/2.1.8/js/dataTables.js' crossorigin="anonymous"></script>
|
||||
<script src='https://cdn.datatables.net/2.1.8/js/dataTables.bootstrap4.js' crossorigin="anonymous"></script>
|
||||
<link rel="stylesheet" href="https://cdn.datatables.net/2.1.8/css/dataTables.bootstrap4.css" crossorigin="anonymous">
|
||||
<div class="row w-100 m-0">
|
||||
<form class="ml-2 form-inline w-100" action="/search">
|
||||
<div class="w-100 align-items-center">
|
||||
@@ -24,16 +24,18 @@
|
||||
</pre>
|
||||
</a>
|
||||
<input type="hidden" name="s" id="searchSettings">
|
||||
<input id="search" type="text" class="w-50 form-control bg-dark text-white ml-2" name="q" value="<%= query %>" autocomplete="off">
|
||||
<button type="submit" class="btn btn-secondary ml-2">Search</button>
|
||||
<input id="search" type="text" class="w-50 form-control bg-dark text-white ml-2" name="q" value="<%= query %>" autocomplete="off" placeholder="<%= __('search.placeholder') %>">
|
||||
<button type="submit" class="btn btn-secondary ml-2"><%= __('search.button') %></button>
|
||||
|
||||
</div>
|
||||
<ul class="SuggestionList col-sm-12" id="suggestionList" style="width: 50%;left: 195px;"></ul>
|
||||
</div>
|
||||
<p class="m-2">Found <%= results.items.length %> result<%= results.items.length != 1 ? 's': '' %> in <%= results.elapsed %> seconds. <%= indexing ? "Indexing in progress, if the list is missing something please try reloading in a few minutes" : "" %>
|
||||
<p class="m-2">
|
||||
<%= __('search.found_plural', { count: results.items.length }) %> <%= __('search.in_seconds', { seconds: results.elapsed }) %>.
|
||||
<%= indexing ? __('search.indexing') : "" %>
|
||||
<% if (settings.hideNonGame) { %>
|
||||
<span class="badge badge-info" data-toggle="tooltip" data-placement="top" title="Hiding ROM hacks, patches, and other non-game content. Disable this in Settings.">
|
||||
Non-game content filter is active
|
||||
<span class="badge badge-info" data-toggle="tooltip" data-placement="top" title="<%= __('settings.extras.hide_non_game.tooltip') %>">
|
||||
<%= __('search.non_game_filter') %>
|
||||
<a href="/settings" class="text-white ml-1"><i class="bi bi-gear-fill"></i></a>
|
||||
</span>
|
||||
<% } %>
|
||||
@@ -41,20 +43,20 @@
|
||||
</form>
|
||||
|
||||
<div class="col-sm-12 w-100 mt-3">
|
||||
<p>Displaying results <%= entryStart %> through <%= entryEnd %>. </p>
|
||||
<p><%= __('search.displaying_results', { start: entryStart, end: entryEnd }) %></p>
|
||||
<table class="table text-white table-bordered" id="results">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Group</th>
|
||||
<th>Category</th>
|
||||
<th>Region</th>
|
||||
<th>Type</th>
|
||||
<th>Size</th>
|
||||
<th>Date</th>
|
||||
<th>Search Score</th>
|
||||
<th><%= __('results.table.name') %></th>
|
||||
<th><%= __('results.table.group') %></th>
|
||||
<th><%= __('results.table.category') %></th>
|
||||
<th><%= __('results.table.region') %></th>
|
||||
<th><%= __('results.table.type') %></th>
|
||||
<th><%= __('results.table.size') %></th>
|
||||
<th><%= __('results.table.date') %></th>
|
||||
<th><%= __('results.table.score') %></th>
|
||||
<% if (process.env.EMULATOR_ENABLED === 'true') { %>
|
||||
<th>Play</th>
|
||||
<th><%= __('results.table.play') %></th>
|
||||
<% } %>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -89,9 +91,9 @@
|
||||
<% if (process.env.EMULATOR_ENABLED === 'true') { %>
|
||||
<td>
|
||||
<% if (isEmulatorCompatible(results.items[x].category)) { %>
|
||||
<a href="/play/<%= results.items[x].id %>" class="btn btn-sm btn-secondary">Play</a>
|
||||
<a href="/play/<%= results.items[x].id %>" class="btn btn-sm btn-secondary"><%= __('emulator.play') %></a>
|
||||
<% } else { %>
|
||||
<button class="btn btn-sm btn-secondary" disabled>----</button>
|
||||
<button class="btn btn-sm btn-secondary" disabled><%= __('emulator.not_available') %></button>
|
||||
<% } %>
|
||||
</td>
|
||||
<% } %>
|
||||
|
||||
@@ -2,17 +2,17 @@
|
||||
<div class="col-sm-12 my-auto">
|
||||
<pre style="font: 20px / 19px monospace; color: white; text-align: center; overflow: hidden;">
|
||||
<%= generateAsciiArt() %>
|
||||
Search!
|
||||
<%= __('nav.search') %>!
|
||||
</pre>
|
||||
<div class="text-center text-white">
|
||||
<form>
|
||||
<input type="hidden" name="s" id="searchSettings">
|
||||
<input id="search" type="text" style="width: 80%;display: inline;" class="form-control bg-dark text-white mb-2"
|
||||
name="q" autocomplete="off">
|
||||
name="q" autocomplete="off" placeholder="<%= __('search.placeholder') %>">
|
||||
<ul class="SuggestionList col-sm-12" id="suggestionList" style="width: 78%;left: 11%;"></ul>
|
||||
<div>
|
||||
<button type="submit" formaction="/search" class="btn btn-secondary">Myrient Search</button>
|
||||
<button type="submit" formaction="/lucky" class="btn btn-secondary">I'm Feeling Lucky</button>
|
||||
<button type="submit" formaction="/search" class="btn btn-secondary"><%= __('search.button') %></button>
|
||||
<button type="submit" formaction="/lucky" class="btn btn-secondary"><%= __('search.lucky') %></button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -5,12 +5,12 @@
|
||||
<div class="col-sm-12 my-auto text-center">
|
||||
<pre style="font: 20px / 19px monospace; color: white; text-align: center; overflow: hidden;">
|
||||
<%= generateAsciiArt() %>
|
||||
Settings
|
||||
<%= __('settings.title') %>
|
||||
</pre>
|
||||
<div class="card w-auto mx-auto text-center d-inline-block p-3">
|
||||
<form>
|
||||
<div class="form-group">
|
||||
<h4 class="d-inline mr-2">Search Columns</h4><i class="bi bi-question-circle" data-toggle="tooltip" data-placement="top" title="Selects which columns the search engine will search on."></i>
|
||||
<h4 class="d-inline mr-2"><%= __('settings.search_columns.title') %></h4><i class="bi bi-question-circle" data-toggle="tooltip" data-placement="top" title="<%= __('settings.search_columns.tooltip') %>"></i>
|
||||
<div class="">
|
||||
<% for(let field in defaultSettings.fields) { %>
|
||||
<label class="checkbox-inline p-1" for="<%= defaultSettings.fields[field] %>">
|
||||
@@ -21,7 +21,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<h4 class="d-inline mr-2">Search Score Multiplier</h4><i class="bi bi-question-circle" data-toggle="tooltip" data-placement="top" title="Multiplies the match score for each word found based on the category it's found in."></i>
|
||||
<h4 class="d-inline mr-2"><%= __('settings.score_multiplier.title') %></h4><i class="bi bi-question-circle" data-toggle="tooltip" data-placement="top" title="<%= __('settings.score_multiplier.tooltip') %>"></i>
|
||||
<div class="">
|
||||
<% for(let field in defaultSettings.boost) { %>
|
||||
<div class="d-inline-block">
|
||||
@@ -32,10 +32,10 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<h4>Extras</h4>
|
||||
<h4><%= __('settings.extras.title') %></h4>
|
||||
<div class="form-group">
|
||||
<div class="d-inline-block">
|
||||
<label for="fuzzy">Fuzzy Value <i class="bi bi-question-circle" data-toggle="tooltip" data-placement="top" title="Value between 0.00 and 1.00 that determines the fuzzy distance (Levenshtein distance) for how closely a word needs to be considered a match. A higher value allows for less stringent matches. A value of 0 disables. "></i></label>
|
||||
<label for="fuzzy"><%= __('settings.extras.fuzzy.label') %> <i class="bi bi-question-circle" data-toggle="tooltip" data-placement="top" title="<%= __('settings.extras.fuzzy.tooltip') %>"></i></label>
|
||||
<input type="number" class="form-control bg-dark text-white" id="fuzzy" name="fuzzy" step="0.01" min="0" max="1">
|
||||
</div>
|
||||
</div>
|
||||
@@ -43,131 +43,131 @@
|
||||
<div class="">
|
||||
<label class="checkbox-inline p-1">
|
||||
<input type="checkbox" id="prefix" value="true">
|
||||
Allow Prefixes <i class="bi bi-question-circle" data-toggle="tooltip" data-placement="top" title="Allows partial matches of words at the start of the word."></i>
|
||||
<%= __('settings.extras.prefix.label') %> <i class="bi bi-question-circle" data-toggle="tooltip" data-placement="top" title="<%= __('settings.extras.prefix.tooltip') %>"></i>
|
||||
</label>
|
||||
<label class="checkbox-inline p-1">
|
||||
<input type="checkbox" id="combineWith" value="AND">
|
||||
Match All Words <i class="bi bi-question-circle" data-toggle="tooltip" data-placement="top" title="Requires all words in the search query to match."></i>
|
||||
<%= __('settings.extras.match_all.label') %> <i class="bi bi-question-circle" data-toggle="tooltip" data-placement="top" title="<%= __('settings.extras.match_all.tooltip') %>"></i>
|
||||
</label>
|
||||
<label class="checkbox-inline p-1">
|
||||
<input type="checkbox" id="hideNonGame" value="true">
|
||||
Hide Non-Game Content <i class="bi bi-question-circle" data-toggle="tooltip" data-placement="top" title="Filters out ROM hacks, patches, artwork, and other non-game content from search results."></i>
|
||||
<%= __('settings.extras.hide_non_game.label') %> <i class="bi bi-question-circle" data-toggle="tooltip" data-placement="top" title="<%= __('settings.extras.hide_non_game.tooltip') %>"></i>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-secondary mb-2" action="#" id="saveSettings">Save Settings</button>
|
||||
<button type="button" class="btn btn-secondary mb-2" action="#" id="saveSettings"><%= __('settings.save') %></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script defer>
|
||||
defaults = <%-JSON.stringify(defaultSettings)%>
|
||||
settingStore = localStorage.getItem('settings')
|
||||
settings = undefined
|
||||
<script defer>
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
//combinewith fuzzy prefix
|
||||
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 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 toggleLinkedTextBox(checkBox, textBox){
|
||||
if(! checkBox.checked) {
|
||||
textBox.classList.add('text-secondary')
|
||||
textBox.disabled = true
|
||||
}
|
||||
else {
|
||||
textBox.classList.remove('text-secondary')
|
||||
textBox.disabled = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
</script>
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<div class="mb-2">
|
||||
<div class="text-center text-secondary footer-text">
|
||||
<div id="query-count" class="stats">Number of Queries:</div>
|
||||
<div id="query-count" class="stats"><%= __('footer.queries') %></div>
|
||||
<div class="stats"> | </div>
|
||||
<div id="file-count" class="stats">Known Files:</div>
|
||||
<div id="file-count" class="stats"><%= __('footer.files') %></div>
|
||||
<div class="stats"> | </div>
|
||||
<div id="term-count" class="stats">Term Count:</div>
|
||||
<div id="term-count" class="stats"><%= __('footer.terms') %></div>
|
||||
<div class="stats"> | </div>
|
||||
<div id="crawl-time" class="stats">Time of Last Crawl:</div>
|
||||
<div id="crawl-time" class="stats"><%= __('footer.last_crawl') %></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
<title><%= process.env.INSTANCE_NAME || 'Myrient' %> Search</title>
|
||||
|
||||
<!-- CSS (load bootstrap from a CDN) -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.5.2/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
|
||||
<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">
|
||||
<style>
|
||||
|
||||
html, body {
|
||||
|
||||
@@ -1,20 +1,44 @@
|
||||
<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">Results</a>
|
||||
<a id="brand-name" class="navbar-brand text-white hidden" href="/search"><%= __('nav.results') %></a>
|
||||
<ul class="navbar-nav mr-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-white" href="/settings">Settings</a>
|
||||
<a class="nav-link text-white" href="/settings"><%= __('nav.settings') %></a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-white" href="/emulators">Emulators</a>
|
||||
<a class="nav-link text-white" href="/emulators"><%= __('nav.emulators') %></a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-white" href="/about">About</a>
|
||||
<a class="nav-link text-white" href="/about"><%= __('nav.about') %></a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- Language Selector Dropdown -->
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-dark dropdown-toggle" type="button" id="languageDropdown" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<i class="bi bi-globe"></i>
|
||||
<%= __('languages.' + locale) %>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right bg-dark" aria-labelledby="languageDropdown">
|
||||
<% availableLocales.forEach(lang => { %>
|
||||
<a class="dropdown-item text-white <%= locale === lang ? 'active' : '' %>" href="javascript:void(0)" onclick="changeLanguage('<%= lang %>')">
|
||||
<%= __('languages.' + lang) %>
|
||||
</a>
|
||||
<% }); %>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<script defer>
|
||||
<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){
|
||||
@@ -22,4 +46,35 @@
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user