Finished theming

This commit is contained in:
2025-09-06 22:28:11 -04:00
parent dae849bb90
commit bda6f999b7
5 changed files with 110 additions and 122 deletions

View File

@@ -55,17 +55,17 @@
<h2 class="text-2xl font-bold mb-6">System Management</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-8">
<!-- ROM Scanning -->
<!-- Game Scanning -->
<div class="bg-secondary rounded-lg p-6 border border-theme">
<div class="flex items-center mb-4">
<svg class="w-8 h-8 text-accent mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
<h3 class="text-lg font-semibold">ROM Scanner</h3>
<h3 class="text-lg font-semibold">Game Scanner</h3>
</div>
<p class="text-secondary text-sm mb-4">Scan directories for new ROM files and add them to the database</p>
<button onclick="triggerRomScan()" class="w-full bg-accent hover:bg-accent px-4 py-2 rounded font-medium transition-colors" id="scan-btn">
<span class="scan-text">Start ROM Scan</span>
<p class="text-secondary text-sm mb-4">Scan directories for new Game files and add them to the database</p>
<button onclick="triggerGameScan()" class="w-full bg-accent hover:bg-accent px-4 py-2 rounded font-medium transition-colors" id="scan-btn">
<span class="scan-text">Start Game Scan</span>
<span class="scan-loading hidden">Scanning...</span>
</button>
<div id="scan-status" class="mt-2 text-sm text-secondary"></div>
@@ -318,8 +318,8 @@
<h4 class="text-lg font-semibold text-accent">Paths</h4>
<div>
<label class="block text-sm font-medium text-secondary mb-1">ROM Directory</label>
<input type="text" id="config-rom-path" class="w-full px-3 py-2 bg-tertiary border border-theme rounded text-primary" placeholder="/path/to/roms">
<label class="block text-sm font-medium text-secondary mb-1">Game Directory</label>
<input type="text" id="config-rom-path" class="w-full px-3 py-2 bg-tertiary border border-theme rounded text-primary" placeholder="/path/to/games">
</div>
<div>
@@ -415,7 +415,7 @@
</div>
<script>
async function triggerRomScan() {
async function triggerGameScan() {
const btn = document.getElementById('scan-btn');
const status = document.getElementById('scan-status');
const scanText = btn.querySelector('.scan-text');
@@ -426,7 +426,7 @@ async function triggerRomScan() {
scanLoading.classList.remove('hidden');
try {
const response = await fetch('/api/admin/rom-scan', {
const response = await fetch('/api/admin/game-scan', {
method: 'POST',
headers: {
'Authorization': `Bearer ${getCookie('auth_token')}`
@@ -436,17 +436,17 @@ async function triggerRomScan() {
const result = await response.json();
if (result.status === 'started') {
status.textContent = 'ROM scan started...';
status.textContent = 'Game scan started...';
status.className = 'mt-2 text-sm text-accent';
// Poll for completion
pollTaskStatus('rom_scan', status);
pollTaskStatus('game_scan', status);
} else if (result.status === 'already_running') {
status.textContent = 'ROM scan already in progress';
status.textContent = 'Game scan already in progress';
status.className = 'mt-2 text-sm text-warning-color';
}
} catch (error) {
status.textContent = 'Failed to start ROM scan';
status.textContent = 'Failed to start Game scan';
status.className = 'mt-2 text-sm text-danger-color';
} finally {
btn.disabled = false;
@@ -674,8 +674,8 @@ async function showSystemStats() {
<h4 class="text-lg font-semibold mb-3 text-primary">Running Tasks</h4>
<div class="space-y-2 text-secondary">
<div class="flex justify-between">
<span>ROM Scan:</span>
<span class="${stats.running_tasks.rom_scan ? 'text-accent' : 'text-secondary'}">${stats.running_tasks.rom_scan ? 'Running' : 'Idle'}</span>
<span>Game Scan:</span>
<span class="${stats.running_tasks.game_scan ? 'text-accent' : 'text-secondary'}">${stats.running_tasks.game_scan ? 'Running' : 'Idle'}</span>
</div>
<div class="flex justify-between">
<span>Metadata Refresh:</span>
@@ -1307,7 +1307,7 @@ function populateConfigForm(config) {
// Populate form fields
document.getElementById('config-host').value = config.host || '';
document.getElementById('config-port').value = config.port || '';
document.getElementById('config-rom-path').value = config.rom_path || '';
document.getElementById('config-rom-path').value = config.game_path || '';
document.getElementById('config-images-path').value = config.images_path || '';
document.getElementById('config-igdb-client-id').value = config.igdb_client_id || '';
document.getElementById('config-igdb-secret').value = config.igdb_api_key || '';
@@ -1320,7 +1320,7 @@ function collectConfigFromForm() {
const config = {
host: document.getElementById('config-host').value,
port: parseInt(document.getElementById('config-port').value) || 8080,
rom_path: document.getElementById('config-rom-path').value,
game_path: document.getElementById('config-rom-path').value,
images_path: document.getElementById('config-images-path').value,
igdb_client_id: document.getElementById('config-igdb-client-id').value,
igdb_api_key: document.getElementById('config-igdb-secret').value

View File

@@ -197,6 +197,8 @@
.bg-tertiary { background-color: var(--tertiary-bg); }
.bg-accent { background-color: var(--accent-bg); }
.bg-accent:hover { background-color: var(--accent-hover); }
.bg-accent-hover { background-color: var(--accent-hover); }
.bg-warning { background-color: var(--warning-color) !important; }
.text-primary { color: var(--text-primary); }
.text-secondary { color: var(--text-secondary); }
.text-accent { color: var(--text-accent); }
@@ -259,7 +261,7 @@
<div class="hidden md:block">
<div class="ml-10 flex items-baseline space-x-4">
<a href="/" class="hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium">
Browse ROMs
Browse Games
</a>
{% if current_user and current_user.role != "demo" %}
<a href="/favorites" class="hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium">
@@ -317,28 +319,23 @@
</div>
{% if current_user %}
<span class="text-sm">
<span class="text-sm text-primary">
Welcome, {{ current_user.username }}
{% if current_user.role == "demo" %}
<span class="bg-yellow-600 px-2 py-1 rounded text-xs">DEMO</span>
{% elif current_user.role == "super" %}
<span class="bg-red-600 px-2 py-1 rounded text-xs">ADMIN</span>
{% endif %}
</span>
<button onclick="logout()" class="bg-red-600 hover:bg-red-700 px-4 py-2 rounded text-sm">
<button onclick="logout()" class="bg-red-600 hover:bg-red-700 px-4 py-2 rounded text-sm transition-colors">
Logout
</button>
{% else %}
<button onclick="showLogin()" class="bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded text-sm">
<button onclick="showLogin()" class="bg-accent hover:bg-accent-hover px-4 py-2 rounded text-sm text-primary transition-colors">
Login
</button>
<span class="bg-yellow-600 px-2 py-1 rounded text-xs">DEMO MODE</span>
<span class="bg-warning px-2 py-1 rounded text-xs text-black font-medium">DEMO MODE</span>
{% endif %}
</div>
<!-- Mobile menu button -->
<div class="md:hidden">
<button onclick="toggleMobileMenu()" class="bg-gray-700 inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white">
<button onclick="toggleMobileMenu()" class="bg-tertiary inline-flex items-center justify-center p-2 rounded-md text-secondary hover:text-primary hover:bg-accent focus:outline-none focus:ring-2 focus:ring-inset focus:ring-accent transition-colors">
<svg id="hamburger-icon" class="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
@@ -354,7 +351,7 @@
<div id="mobile-menu" class="md:hidden hidden">
<div class="px-2 pt-2 pb-3 space-y-1 sm:px-3">
<a href="/" class="hover:bg-gray-700 hover:text-white block px-3 py-2 rounded-md text-base font-medium">
Browse ROMs
Browse Games
</a>
{% if current_user and current_user.role != "demo" %}
<a href="/favorites" class="hover:bg-gray-700 hover:text-white block px-3 py-2 rounded-md text-base font-medium">
@@ -406,24 +403,15 @@
</div>
{% if current_user %}
<div class="text-base font-medium text-white">{{ current_user.username }}</div>
<div class="text-sm text-gray-400 mb-3">
{% if current_user.role == "demo" %}
<span class="bg-yellow-600 px-2 py-1 rounded text-xs">DEMO USER</span>
{% elif current_user.role == "super" %}
<span class="bg-red-600 px-2 py-1 rounded text-xs">ADMIN</span>
{% else %}
<span class="bg-green-600 px-2 py-1 rounded text-xs">USER</span>
{% endif %}
</div>
<button onclick="logout()" class="bg-red-600 hover:bg-red-700 px-4 py-2 rounded text-sm w-full">
<div class="text-base font-medium text-primary">{{ current_user.username }}</div>
<button onclick="logout()" class="bg-red-600 hover:bg-red-700 px-4 py-2 rounded text-sm w-full mt-3 transition-colors">
Logout
</button>
{% else %}
<button onclick="showLogin()" class="bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded text-sm w-full mb-2">
<button onclick="showLogin()" class="bg-accent hover:bg-accent-hover px-4 py-2 rounded text-sm w-full mb-2 text-primary transition-colors">
Login
</button>
<span class="bg-yellow-600 px-2 py-1 rounded text-xs">DEMO MODE</span>
<span class="bg-warning px-2 py-1 rounded text-xs text-black font-medium">DEMO MODE</span>
{% endif %}
</div>
</div>
@@ -431,14 +419,14 @@
</nav>
<!-- Genres Sidebar -->
<div id="genresSidebar" class="fixed inset-y-0 left-0 w-80 bg-gray-800 border-r border-gray-700 transform -translate-x-full transition-transform duration-300 ease-in-out z-50">
<div id="genresSidebar" class="fixed inset-y-0 left-0 w-80 bg-secondary border-r border-theme transform -translate-x-full transition-transform duration-300 ease-in-out z-50">
<div class="h-full flex flex-col p-4">
<div class="flex justify-between items-center mb-4 flex-shrink-0">
<h3 class="text-lg font-medium text-white">Browse by Genre</h3>
<button onclick="toggleGenresSidebar()" class="text-gray-400 hover:text-white">&times;</button>
<h3 class="text-lg font-medium text-primary">Browse by Genre</h3>
<button onclick="toggleGenresSidebar()" class="text-secondary hover:text-primary">&times;</button>
</div>
<div id="genresContainer" class="space-y-2 flex-1 overflow-y-auto">
<p class="text-gray-400 text-sm">Loading genres...</p>
<p class="text-secondary text-sm">Loading genres...</p>
</div>
</div>
</div>
@@ -451,27 +439,27 @@
</main>
<!-- Login Modal -->
<div id="loginModal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
<div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-gray-800 border-gray-700">
<div id="loginModal" class="hidden fixed inset-0 bg-black bg-opacity-50 overflow-y-auto h-full w-full z-50">
<div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-secondary border-theme">
<div class="mt-3">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-medium text-white">Login</h3>
<button onclick="hideLogin()" class="text-gray-400 hover:text-white">&times;</button>
<h3 class="text-lg font-medium text-primary">Login</h3>
<button onclick="hideLogin()" class="text-secondary hover:text-primary">&times;</button>
</div>
<form onsubmit="handleLogin(event)" class="space-y-4">
<div>
<input type="text" id="username" placeholder="Username" required
class="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-md text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500">
class="w-full px-3 py-2 bg-tertiary border border-theme rounded-md text-primary placeholder-secondary focus:outline-none focus:ring-2 focus:ring-accent">
</div>
<div>
<input type="password" id="password" placeholder="Password" required
class="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-md text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500">
class="w-full px-3 py-2 bg-tertiary border border-theme rounded-md text-primary placeholder-secondary focus:outline-none focus:ring-2 focus:ring-accent">
</div>
<div class="flex justify-between">
<button type="button" onclick="hideLogin()" class="px-4 py-2 bg-gray-600 hover:bg-gray-700 rounded-md">
<button type="button" onclick="hideLogin()" class="px-4 py-2 bg-tertiary hover:bg-accent rounded-md text-primary transition-colors">
Cancel
</button>
<button type="submit" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-md">
<button type="submit" class="px-4 py-2 bg-accent hover:bg-accent-hover rounded-md text-primary transition-colors">
Login
</button>
</div>
@@ -613,7 +601,7 @@
const container = document.getElementById('genresContainer');
if (genres.length === 0) {
container.innerHTML = '<p class="text-gray-400 text-sm">No genres found</p>';
container.innerHTML = '<p class="text-secondary text-sm">No genres found</p>';
return;
}
@@ -621,8 +609,8 @@
.sort((a, b) => b.count - a.count)
.map(genre => `
<a href="/browse/genres/${encodeURIComponent(genre.name)}"
class="block p-2 bg-gray-700 hover:bg-gray-600 rounded text-sm text-white">
${genre.name} <span class="text-gray-400">(${genre.count})</span>
class="block p-2 bg-tertiary hover:bg-accent rounded text-sm text-primary transition-colors">
${genre.name} <span class="text-secondary">(${genre.count})</span>
</a>
`).join('');

View File

@@ -5,7 +5,7 @@
{% block content %}
<div class="mb-8">
<h1 class="text-3xl font-bold mb-2">My Favorites</h1>
<p class="text-gray-400">Your personally selected ROM collection</p>
<p class="text-gray-400">Your personally selected game collection</p>
</div>
{% if games %}
@@ -86,7 +86,7 @@
<h2 class="text-2xl font-bold mb-2">No favorites yet</h2>
<p class="text-gray-400 mb-6">Start browsing and add games to your favorites collection!</p>
<a href="/" class="bg-blue-600 hover:bg-blue-700 px-6 py-3 rounded-lg text-white">
Browse ROMs
Browse Games
</a>
</div>
{% endif %}
@@ -130,7 +130,10 @@
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = response.headers.get('Content-Disposition')?.split('filename=')[1] || 'game.zip';
// Get filename from Content-Disposition header, removing quotes and underscores
let filename = response.headers.get('Content-Disposition')?.split('filename=')[1] || 'game.zip';
filename = filename.replace(/^["_]+|["_]+$/g, ''); // Remove quotes and underscores from start and end
a.download = filename;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);

View File

@@ -6,7 +6,7 @@
<div class="max-w-4xl mx-auto">
<div class="mb-6">
<nav class="text-sm text-gray-400 mb-4">
<a href="/" class="hover:text-white">Browse ROMs</a>
<a href="/" class="hover:text-white">Browse Games</a>
<span class="mx-2">/</span>
<span class="text-white">{{ game.metadata_obj.title or game.title }}</span>
</nav>
@@ -36,7 +36,7 @@
{% if can_download %}
<button onclick="downloadGame({{ game.id }})"
class="bg-green-600 hover:bg-green-700 px-4 py-2 rounded">
Download ROM
Download Game
</button>
{% else %}
<span class="bg-gray-600 px-4 py-2 rounded cursor-not-allowed">
@@ -268,7 +268,10 @@
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = response.headers.get('Content-Disposition')?.split('filename=')[1] || 'game.zip';
// Get filename from Content-Disposition header, removing quotes and underscores
let filename = response.headers.get('Content-Disposition')?.split('filename=')[1] || 'game.zip';
filename = filename.replace(/^["_]+|["_]+$/g, ''); // Remove quotes and underscores from start and end
a.download = filename;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);

View File

@@ -1,11 +1,11 @@
{% extends "base.html" %}
{% block title %}ROM Library - DOS Frontend{% endblock %}
{% block title %}Game Library - DOS Frontend{% endblock %}
{% block content %}
<div class="mb-6">
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-4">
<h1 class="text-3xl font-bold mb-2 sm:mb-0">ROM Library</h1>
<h1 class="text-3xl font-bold mb-2 sm:mb-0">Game Library</h1>
<div class="flex flex-col sm:flex-row gap-2">
<!-- Search Form -->
<form method="GET" class="flex gap-2">
@@ -15,26 +15,26 @@
<div class="relative">
<input type="text" name="search" placeholder="Search games..."
value="{% if search and not search.startswith('genre:') and not search.startswith('tag:') %}{{ search }}{% endif %}"
class="w-full sm:w-64 px-4 py-2 pl-10 bg-gray-700 border border-gray-600 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500">
class="w-full sm:w-64 px-4 py-2 pl-10 bg-tertiary border border-theme rounded-lg text-primary placeholder-secondary focus:outline-none focus:ring-2 focus:ring-accent">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-4 h-4 text-secondary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m21 21-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
</div>
</div>
<button type="submit" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg text-white">Search</button>
<button type="submit" class="px-4 py-2 bg-accent hover:bg-accent-hover rounded-lg text-primary transition-colors">Search</button>
</form>
</div>
</div>
<!-- Controls Bar -->
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-4">
<div class="text-gray-400">
<div class="text-secondary">
{% if is_demo %}
<span class="text-yellow-400">Demo Mode:</span> You can browse ROMs but cannot download or favorite them.
<span class="text-yellow-400">Demo Mode:</span> You can browse games but cannot download or favorite them.
<button onclick="showLogin()" class="text-blue-400 hover:text-blue-300 underline">Login</button> for full access.
{% else %}
Showing {{ games|length }} of {{ total_games }} ROMs
Showing {{ games|length }} of {{ total_games }} games
{% if search %} for "{{ search }}"{% endif %}
{% endif %}
</div>
@@ -42,8 +42,8 @@
<div class="flex flex-col sm:flex-row gap-2 sm:gap-4 items-center">
<!-- Results per page -->
<div class="flex items-center gap-2">
<label class="text-gray-400 text-sm">Show:</label>
<select onchange="changePerPage(this.value)" class="bg-gray-700 border border-gray-600 rounded px-2 py-1 text-white text-sm min-w-16">
<label class="text-secondary text-sm">Show:</label>
<select onchange="changePerPage(this.value)" class="bg-tertiary border border-theme rounded px-2 py-1 text-primary text-sm min-w-16">
<option value="20" {% if per_page == 20 %}selected{% endif %}>20</option>
<option value="50" {% if per_page == 50 %}selected{% endif %}>50</option>
<option value="100" {% if per_page == 100 %}selected{% endif %}>100</option>
@@ -51,16 +51,16 @@
</div>
<!-- View Toggle -->
<div class="flex bg-gray-700 rounded-lg p-1">
<div class="flex bg-tertiary rounded-lg p-1">
<button onclick="changeView('grid')"
class="px-4 py-2 rounded text-sm touch-manipulation {{ 'bg-blue-600 text-white' if view == 'grid' else 'text-gray-400 hover:text-white active:bg-gray-600' }}">
class="px-4 py-2 rounded text-sm touch-manipulation transition-colors {{ 'bg-accent text-primary' if view == 'grid' else 'text-secondary hover:text-primary active:bg-secondary' }}">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path d="M5 3a2 2 0 00-2 2v2a2 2 0 002 2h2a2 2 0 002-2V5a2 2 0 00-2-2H5zM5 11a2 2 0 00-2 2v2a2 2 0 002 2h2a2 2 0 002-2v-2a2 2 0 00-2-2H5zM11 5a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V5zM11 13a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"></path>
</svg>
<span class="ml-1 hidden sm:inline">Grid</span>
</button>
<button onclick="changeView('list')"
class="px-4 py-2 rounded text-sm touch-manipulation {{ 'bg-blue-600 text-white' if view == 'list' else 'text-gray-400 hover:text-white active:bg-gray-600' }}">
class="px-4 py-2 rounded text-sm touch-manipulation transition-colors {{ 'bg-accent text-primary' if view == 'list' else 'text-secondary hover:text-primary active:bg-secondary' }}">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M3 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" clip-rule="evenodd"></path>
</svg>
@@ -75,29 +75,29 @@
{% if view == 'grid' %}
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4">
{% for game in games %}
<div class="bg-gray-800 rounded-lg border border-gray-700 hover:border-gray-600 transition-colors overflow-hidden hover:shadow-lg transform hover:-translate-y-1 transition-all duration-200 relative group">
<div class="bg-secondary rounded-lg border border-theme hover:border-accent transition-colors overflow-hidden hover:shadow-lg transform hover:-translate-y-1 transition-all duration-200 relative group">
<!-- Clickable overlay for the card -->
<div onclick="showGameDetail({{ game.id }})" class="absolute inset-0 z-10 cursor-pointer" aria-label="View {{ game.metadata_obj.title or game.title }} details"></div>
<!-- Cover Image -->
<div class="aspect-[3/4] bg-gray-900 relative overflow-hidden">
<div class="aspect-[3/4] bg-primary relative overflow-hidden">
{% if game.metadata_obj and (game.metadata_obj.cover_image_path or (game.metadata_obj.cover_image and game.metadata_obj.cover_image.startswith('http'))) %}
<img data-game-id="{{ game.id }}"
{% if game.metadata_obj.cover_image_path %}src="/images/{{ game.metadata_obj.cover_image_path.name }}"{% else %}src="{{ game.metadata_obj.cover_image }}"{% endif %}
alt="{{ game.metadata_obj.title or game.title }}"
class="w-full h-full object-cover transition-transform duration-200 group-hover:scale-105"
onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
<div class="w-full h-full bg-gradient-to-br from-gray-800 to-gray-900 hidden items-center justify-center">
<div class="text-gray-400 text-center p-4">
<div class="w-full h-full bg-gradient-theme hidden items-center justify-center absolute inset-0">
<div class="text-secondary text-center p-4">
<svg class="w-16 h-16 mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2h2a2 2 0 002-2z"></path>
</svg>
<p class="text-sm font-medium">{{ (game.metadata_obj.title or game.title)[:20] }}{% if (game.metadata_obj.title or game.title)|length > 20 %}...{% endif %}</p>
<p class="text-xs text-gray-500 mt-1">DOS Game</p>
<p class="text-xs text-secondary opacity-75 mt-1">DOS Game</p>
</div>
</div>
{% else %}
<div class="w-full h-full bg-gradient-theme flex items-center justify-center relative overflow-hidden">
<div class="w-full h-full bg-gradient-theme flex items-center justify-center relative overflow-hidden absolute inset-0">
<!-- Background Pattern -->
<div class="absolute inset-0 opacity-10">
<svg width="100%" height="100%" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -111,7 +111,7 @@
</div>
<!-- Main Content -->
<div class="text-center p-3 relative z-10">
<div class="text-center p-3 relative z-10 w-full h-full flex flex-col justify-center items-center">
<!-- DosVault Logo -->
<div class="mb-4">
<svg class="w-20 h-20 mx-auto text-accent" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -188,23 +188,23 @@
</div>
<!-- Game Info -->
<div class="p-3">
<h3 class="font-semibold text-blue-400 truncate mb-1 text-sm">
<div class="p-3 bg-tertiary border-t border-theme">
<h3 class="font-semibold text-accent truncate mb-1 text-sm">
{{ game.metadata_obj.title or game.title }}
</h3>
{% if game.metadata_obj and game.metadata_obj.year %}
<p class="text-xs text-gray-400 mb-2">{{ game.metadata_obj.year }}</p>
<p class="text-xs text-secondary mb-2">{{ game.metadata_obj.year }}</p>
{% endif %}
<div class="flex justify-between items-center">
<span class="text-xs text-gray-500">Click to view details</span>
<span class="text-xs text-secondary">Click to view details</span>
{% if not is_demo %}
<button onclick="event.stopPropagation(); downloadGame({{ game.id }})"
class="bg-green-600 hover:bg-green-700 px-2 py-1 rounded text-xs z-20 relative">
class="bg-green-600 hover:bg-green-700 px-2 py-1 rounded text-xs z-20 relative transition-colors">
Download
</button>
{% else %}
<span class="bg-gray-600 px-2 py-1 rounded text-xs cursor-not-allowed">
<span class="bg-tertiary border border-theme px-2 py-1 rounded text-xs cursor-not-allowed text-secondary">
Login
</span>
{% endif %}
@@ -219,20 +219,20 @@
{% if view == 'list' %}
<div class="space-y-2">
{% for game in games %}
<div class="bg-gray-800 rounded-lg border border-gray-700 hover:border-gray-600 transition-colors hover:shadow-lg transform hover:-translate-y-1 transition-all duration-200 relative group">
<div class="bg-secondary rounded-lg border border-theme hover:border-accent transition-colors hover:shadow-lg transform hover:-translate-y-1 transition-all duration-200 relative group">
<!-- Clickable overlay for the card -->
<div onclick="showGameDetail({{ game.id }})" class="absolute inset-0 z-10 cursor-pointer" aria-label="View {{ game.metadata_obj.title or game.title }} details"></div>
<div class="p-4 flex items-center gap-4">
<!-- Cover Image -->
<div class="w-16 h-20 bg-gray-900 rounded overflow-hidden flex-shrink-0 relative">
<div class="w-16 h-20 bg-primary rounded overflow-hidden flex-shrink-0 relative">
{% if game.metadata_obj and (game.metadata_obj.cover_image_path or (game.metadata_obj.cover_image and game.metadata_obj.cover_image.startswith('http'))) %}
<img data-game-id="{{ game.id }}"
{% if game.metadata_obj.cover_image_path %}src="/images/{{ game.metadata_obj.cover_image_path.name }}"{% else %}src="{{ game.metadata_obj.cover_image }}"{% endif %}
alt="{{ game.metadata_obj.title or game.title }}"
class="w-full h-full object-cover"
onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
<div class="w-full h-full bg-gradient-theme hidden items-center justify-center relative overflow-hidden">
<div class="w-full h-full bg-gradient-theme hidden items-center justify-center relative overflow-hidden absolute inset-0">
<!-- Background Pattern -->
<div class="absolute inset-0 opacity-10">
<svg width="100%" height="100%" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -246,10 +246,10 @@
</div>
<!-- Main Content -->
<div class="text-center p-3 relative z-10">
<div class="text-center p-1 relative z-10 w-full h-full flex flex-col justify-center items-center">
<!-- DosVault Logo -->
<div class="mb-4">
<svg class="w-20 h-20 mx-auto text-accent" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<div class="mb-1">
<svg class="w-6 h-6 mx-auto text-accent" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- Vault door with enhanced styling -->
<circle cx="16" cy="16" r="15" fill="currentColor" opacity="0.15" stroke="currentColor" stroke-width="0.5"/>
<circle cx="16" cy="16" r="12" stroke="currentColor" stroke-width="1.5" fill="none" opacity="0.8"/>
@@ -273,18 +273,9 @@
</svg>
</div>
<!-- Game Title -->
<div class="mb-2">
<h3 class="text-accent font-bold text-sm leading-tight mb-1">
{{ (game.metadata_obj.title or game.title)[:18] }}{% if (game.metadata_obj.title or game.title)|length > 18 %}...{% endif %}
</h3>
<div class="w-12 h-0.5 bg-accent mx-auto opacity-60"></div>
</div>
<!-- Branding -->
<div class="text-xs text-secondary opacity-75 font-medium">
<div class="mb-1">CLASSIC DOS GAME</div>
<div class="text-accent text-[10px] font-bold tracking-wider">DOSVAULT</div>
<div class="text-[6px] text-accent font-bold tracking-wide opacity-75">
DOSVAULT
</div>
<!-- Decorative Corner Elements -->
@@ -311,9 +302,9 @@
</div>
</div>
{% else %}
<div class="w-full h-full bg-gradient-theme flex items-center justify-center relative overflow-hidden">
<div class="w-full h-full bg-gradient-theme flex items-center justify-center relative overflow-hidden absolute inset-0">
<!-- Compact Background Pattern -->
<div class="absolute inset-0 opacity-8">
<div class="absolute inset-0 opacity-10">
<svg width="100%" height="100%" viewBox="0 0 32 32" fill="none">
<defs>
<pattern id="pixel-grid-small" x="0" y="0" width="8" height="8" patternUnits="userSpaceOnUse">
@@ -325,8 +316,8 @@
</div>
<!-- Compact Logo -->
<div class="text-center relative z-10">
<svg class="w-8 h-8 mx-auto text-accent mb-1" viewBox="0 0 32 32" fill="none">
<div class="text-center relative z-10 w-full h-full flex flex-col justify-center items-center p-1">
<svg class="w-6 h-6 mx-auto text-accent mb-1" viewBox="0 0 32 32" fill="none">
<!-- Simplified vault door -->
<circle cx="16" cy="16" r="12" stroke="currentColor" stroke-width="1.5" fill="currentColor" opacity="0.1"/>
<circle cx="16" cy="16" r="8" stroke="currentColor" stroke-width="1" opacity="0.6"/>
@@ -342,7 +333,7 @@
<rect x="20" y="14" width="6" height="4" rx="2" fill="currentColor" opacity="0.7"/>
<circle cx="22" cy="16" r="0.8" fill="var(--primary-bg)"/>
</svg>
<div class="text-accent text-[8px] font-bold tracking-wide opacity-75">DOSVAULT</div>
<div class="text-accent text-[6px] font-bold tracking-wide opacity-75">DOSVAULT</div>
</div>
</div>
{% endif %}
@@ -401,7 +392,7 @@
<nav class="flex items-center space-x-1">
{% if current_page > 1 %}
<a href="javascript:void(0)" onclick="goToPage({{ current_page - 1 }})"
class="px-3 py-2 bg-gray-700 hover:bg-gray-600 rounded text-sm">
class="px-3 py-2 bg-tertiary hover:bg-accent rounded text-sm transition-colors">
Previous
</a>
{% endif %}
@@ -409,21 +400,21 @@
<!-- First page -->
{% if current_page > 6 %}
<a href="javascript:void(0)" onclick="goToPage(1)"
class="px-3 py-2 bg-gray-700 hover:bg-gray-600 rounded text-sm">
class="px-3 py-2 bg-tertiary hover:bg-accent rounded text-sm transition-colors">
1
</a>
{% if current_page > 7 %}
<span class="px-3 py-2 text-gray-400">...</span>
<span class="px-3 py-2 text-secondary">...</span>
{% endif %}
{% endif %}
<!-- Page numbers around current page -->
{% for page_num in range(max(1, current_page - 4), min(total_pages + 1, current_page + 5)) %}
{% if page_num == current_page %}
<span class="px-3 py-2 bg-blue-600 rounded text-sm">{{ page_num }}</span>
<span class="px-3 py-2 bg-accent rounded text-sm text-primary">{{ page_num }}</span>
{% else %}
<a href="javascript:void(0)" onclick="goToPage({{ page_num }})"
class="px-3 py-2 bg-gray-700 hover:bg-gray-600 rounded text-sm">
class="px-3 py-2 bg-tertiary hover:bg-accent rounded text-sm transition-colors">
{{ page_num }}
</a>
{% endif %}
@@ -432,17 +423,17 @@
<!-- Last page -->
{% if current_page < total_pages - 5 %}
{% if current_page < total_pages - 6 %}
<span class="px-3 py-2 text-gray-400">...</span>
<span class="px-3 py-2 text-secondary">...</span>
{% endif %}
<a href="javascript:void(0)" onclick="goToPage({{ total_pages }})"
class="px-3 py-2 bg-gray-700 hover:bg-gray-600 rounded text-sm">
class="px-3 py-2 bg-tertiary hover:bg-accent rounded text-sm transition-colors">
{{ total_pages }}
</a>
{% endif %}
{% if current_page < total_pages %}
<a href="javascript:void(0)" onclick="goToPage({{ current_page + 1 }})"
class="px-3 py-2 bg-gray-700 hover:bg-gray-600 rounded text-sm">
class="px-3 py-2 bg-tertiary hover:bg-accent rounded text-sm transition-colors">
Next
</a>
{% endif %}
@@ -454,7 +445,7 @@
<div id="gameDetailOverlay" class="hidden fixed inset-0 bg-black bg-opacity-75 z-50 flex items-center justify-center p-4 overflow-y-auto backdrop-blur-sm">
<div class="bg-secondary rounded-lg max-w-6xl max-h-full w-full overflow-hidden relative shadow-2xl border border-theme animate-fade-in">
<!-- Close Button -->
<button onclick="closeGameDetail()" class="absolute top-4 right-4 text-primary hover:text-secondary text-2xl z-20 bg-primary bg-opacity-20 hover:bg-opacity-30 rounded-full w-10 h-10 flex items-center justify-center transition-all">
<button onclick="closeGameDetail()" class="absolute top-4 right-4 text-primary hover:text-secondary text-2xl z-30 bg-primary bg-opacity-20 hover:bg-opacity-30 rounded-full w-10 h-10 flex items-center justify-center transition-all">
&times;
</button>
@@ -486,7 +477,7 @@
<button id="gameDownloadBtn" onclick="downloadGameFromOverlay()"
class="bg-green-600 hover:bg-green-700 px-4 py-2 rounded hidden">
Download ROM
Download Game
</button>
<span id="gameDownloadDisabled" class="bg-gray-600 px-4 py-2 rounded cursor-not-allowed hidden">
@@ -648,7 +639,10 @@
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = response.headers.get('Content-Disposition')?.split('filename=')[1] || 'game.zip';
// Get filename from Content-Disposition header, removing quotes and underscores
let filename = response.headers.get('Content-Disposition')?.split('filename=')[1] || 'game.zip';
filename = filename.replace(/^["\_]+|["\_]+$/g, ''); // Remove quotes and underscores from start and end
a.download = filename;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);