- 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:
2025-03-31 05:16:43 -03:00
parent 48d716db2d
commit 0138bb0b5e
32 changed files with 3601 additions and 473 deletions

View File

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

View File

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

View File

@@ -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">&times;</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
View 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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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