Updated theming

This commit is contained in:
2025-09-06 18:51:10 -04:00
parent dc7b538be6
commit dae849bb90
11 changed files with 704 additions and 111 deletions

View File

@@ -77,7 +77,7 @@
{% 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">
<!-- Clickable overlay for the card -->
<a href="/games/{{ game.id }}" class="absolute inset-0 z-10 cursor-pointer" aria-label="View {{ game.metadata_obj.title or game.title }} details"></a>
<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">
@@ -221,7 +221,7 @@
{% 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">
<!-- Clickable overlay for the card -->
<a href="/games/{{ game.id }}" class="absolute inset-0 z-10 cursor-pointer" aria-label="View {{ game.metadata_obj.title or game.title }} details"></a>
<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 -->
@@ -450,6 +450,120 @@
</div>
{% endif %}
<!-- Game Detail Overlay -->
<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">
&times;
</button>
<!-- Loading State -->
<div id="gameDetailLoading" class="flex items-center justify-center h-96">
<div class="text-primary">Loading game details...</div>
</div>
<!-- Game Detail Content -->
<div id="gameDetailContent" class="hidden max-h-screen overflow-y-auto">
<!-- Header -->
<div class="border-b border-theme p-6">
<div class="flex justify-between items-start">
<div>
<h1 id="gameTitle" class="text-3xl font-bold mb-2 text-primary"></h1>
<p id="gameFilename" class="text-secondary"></p>
</div>
<div class="flex space-x-3">
<button id="gameFavoriteBtn" onclick="toggleFavoriteInOverlay()"
class="text-red-400 hover:text-red-300 text-2xl hidden">
</button>
<button id="gameEditBtn" onclick="editGame()"
class="bg-yellow-600 hover:bg-yellow-700 px-4 py-2 rounded text-sm hidden">
Edit Metadata
</button>
<button id="gameDownloadBtn" onclick="downloadGameFromOverlay()"
class="bg-green-600 hover:bg-green-700 px-4 py-2 rounded hidden">
Download ROM
</button>
<span id="gameDownloadDisabled" class="bg-gray-600 px-4 py-2 rounded cursor-not-allowed hidden">
Demo Mode - No Downloads
</span>
</div>
</div>
</div>
<!-- Content -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8 p-6">
<div class="lg:col-span-2">
<!-- Description -->
<div id="gameDescription" class="bg-tertiary rounded-lg p-6 border border-theme mb-6 hidden">
<h2 class="text-xl font-bold mb-3 text-primary">Description</h2>
<p id="gameDescriptionText" class="text-secondary leading-relaxed"></p>
</div>
<!-- Game Information -->
<div class="bg-tertiary rounded-lg p-6 border border-theme">
<h2 class="text-xl font-bold mb-4 text-primary">Game Information</h2>
<div id="gameInfoGrid" class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Dynamic content will be inserted here -->
</div>
</div>
</div>
<!-- Sidebar -->
<div class="space-y-6">
<!-- Cover Art -->
<div id="gameCoverArt" class="bg-tertiary rounded-lg p-4 border border-theme hidden">
<h3 class="text-lg font-bold mb-3 text-primary">Cover Art</h3>
<img id="gameCoverImage" src="" alt="" class="w-full rounded">
</div>
<!-- Screenshot -->
<div id="gameScreenshot" class="bg-tertiary rounded-lg p-4 border border-theme hidden">
<h3 class="text-lg font-bold mb-3 text-primary">Screenshot</h3>
<div class="relative group cursor-pointer" onclick="openScreenshotModalInOverlay()">
<img id="gameScreenshotImage" src="" alt="" class="w-full rounded transition-transform duration-200 group-hover:scale-105">
<div class="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-20 rounded transition-all duration-200 flex items-center justify-center">
<svg class="w-12 h-12 text-primary opacity-0 group-hover:opacity-80 transition-opacity duration-200" 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 0zM10 7v3m0 0v3m0-3h3m-3 0H7"></path>
</svg>
</div>
</div>
<p class="text-xs text-secondary mt-2 text-center">Click to enlarge</p>
</div>
<!-- No Metadata Message -->
<div id="gameNoMetadata" class="bg-tertiary rounded-lg p-4 border border-theme text-center hidden">
<div class="text-4xl mb-2">📦</div>
<p class="text-secondary text-sm">No detailed metadata available for this game</p>
<button id="gameAddMetadataBtn" onclick="editGame()"
class="inline-block mt-2 text-accent hover:opacity-80 text-sm underline hidden transition-opacity">
Add Metadata
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Screenshot Modal -->
<div id="overlayScreenshotModal" class="hidden fixed inset-0 bg-black bg-opacity-90 z-60 flex items-center justify-center p-4">
<div class="relative max-w-4xl max-h-full">
<button onclick="closeOverlayScreenshotModal()" class="absolute top-4 right-4 text-primary hover:text-secondary text-3xl z-10 bg-primary bg-opacity-20 hover:bg-opacity-30 rounded-full w-12 h-12 flex items-center justify-center transition-all">
&times;
</button>
<img id="overlayScreenshotModalImage" src="" alt="" class="max-w-full max-h-full rounded-lg">
<div class="absolute bottom-4 left-4 right-4 text-center">
<p id="overlayScreenshotModalTitle" class="text-primary bg-secondary bg-opacity-90 px-4 py-2 rounded-lg"></p>
</div>
</div>
</div>
<script>
function buildUrl(params) {
const url = new URL(window.location);
@@ -606,5 +720,277 @@
// document.addEventListener('DOMContentLoaded', function() {
// // Lazy loading code commented out
// });
// Game Detail Overlay Functions
let currentGameData = null;
async function showGameDetail(gameId) {
const overlay = document.getElementById('gameDetailOverlay');
const loading = document.getElementById('gameDetailLoading');
const content = document.getElementById('gameDetailContent');
// Show overlay and loading state
overlay.classList.remove('hidden');
loading.classList.remove('hidden');
content.classList.add('hidden');
document.body.style.overflow = 'hidden';
try {
const response = await fetch(`/api/games/${gameId}`);
if (!response.ok) {
throw new Error('Failed to fetch game details');
}
currentGameData = await response.json();
populateGameDetail(currentGameData);
// Hide loading, show content
loading.classList.add('hidden');
content.classList.remove('hidden');
} catch (error) {
console.error('Error loading game details:', error);
loading.innerHTML = '<div class="text-red-400">Failed to load game details</div>';
}
}
function populateGameDetail(game) {
// Basic info
document.getElementById('gameTitle').textContent = game.title;
document.getElementById('gameFilename').textContent = game.filename;
// Buttons
const favoriteBtn = document.getElementById('gameFavoriteBtn');
const editBtn = document.getElementById('gameEditBtn');
const downloadBtn = document.getElementById('gameDownloadBtn');
const downloadDisabled = document.getElementById('gameDownloadDisabled');
// Show/hide buttons based on user permissions
if (!game.is_demo) {
favoriteBtn.classList.remove('hidden');
favoriteBtn.textContent = game.is_favorite ? '♥' : '♡';
favoriteBtn.className = game.is_favorite ?
'text-red-600 hover:text-red-300 text-2xl' :
'text-red-400 hover:text-red-300 text-2xl';
} else {
favoriteBtn.classList.add('hidden');
}
if (game.is_super) {
editBtn.classList.remove('hidden');
editBtn.onclick = () => window.open(`/admin/games/${game.id}/edit`, '_blank');
} else {
editBtn.classList.add('hidden');
}
if (game.can_download) {
downloadBtn.classList.remove('hidden');
downloadDisabled.classList.add('hidden');
} else {
downloadBtn.classList.add('hidden');
downloadDisabled.classList.remove('hidden');
downloadDisabled.textContent = game.is_demo ? 'Demo Mode - No Downloads' : 'Login to Download';
}
// Description
const descriptionDiv = document.getElementById('gameDescription');
const descriptionText = document.getElementById('gameDescriptionText');
if (game.metadata.description) {
descriptionText.textContent = game.metadata.description;
descriptionDiv.classList.remove('hidden');
} else {
descriptionDiv.classList.add('hidden');
}
// Game info grid
const infoGrid = document.getElementById('gameInfoGrid');
infoGrid.innerHTML = '';
// Add metadata fields
const metadata = game.metadata;
if (metadata.year) {
addInfoField(infoGrid, 'Release Year', metadata.year);
}
if (metadata.developer) {
addInfoField(infoGrid, 'Developer', metadata.developer);
}
if (metadata.publisher) {
addInfoField(infoGrid, 'Publisher', metadata.publisher);
}
if (metadata.players) {
addInfoField(infoGrid, 'Players', metadata.players);
}
// Genres
if (metadata.genres && metadata.genres.length > 0) {
const genresDiv = document.createElement('div');
genresDiv.className = 'md:col-span-2';
genresDiv.innerHTML = `
<p class="text-gray-400 text-sm mb-2">Genres</p>
<div class="flex flex-wrap gap-2">
${metadata.genres.map(genre =>
`<a href="/browse/genres/${encodeURIComponent(genre.name)}" class="bg-blue-600 hover:bg-blue-700 px-2 py-1 rounded text-sm transition-colors">${genre.name}</a>`
).join('')}
</div>
`;
infoGrid.appendChild(genresDiv);
}
// Tags
if (metadata.tags && metadata.tags.length > 0) {
const tagsDiv = document.createElement('div');
tagsDiv.className = 'md:col-span-2';
tagsDiv.innerHTML = `
<p class="text-gray-400 text-sm mb-2">Tags</p>
<div class="flex flex-wrap gap-2">
${metadata.tags.map(tag =>
`<span class="bg-gray-600 px-2 py-1 rounded text-sm">${tag.name}</span>`
).join('')}
</div>
`;
infoGrid.appendChild(tagsDiv);
}
// File path
addInfoField(infoGrid, 'File Path', game.filepath, 'md:col-span-2', 'font-mono text-sm bg-tertiary text-primary p-2 rounded');
// Cover art
const coverArt = document.getElementById('gameCoverArt');
const coverImage = document.getElementById('gameCoverImage');
if (metadata.cover_image) {
const imageSrc = metadata.cover_image_local ?
`/images/${metadata.cover_image}` :
metadata.cover_image;
coverImage.src = imageSrc;
coverImage.alt = `${game.title} cover`;
coverArt.classList.remove('hidden');
} else {
coverArt.classList.add('hidden');
}
// Screenshot
const screenshot = document.getElementById('gameScreenshot');
const screenshotImage = document.getElementById('gameScreenshotImage');
if (metadata.screenshot) {
const imageSrc = metadata.screenshot_local ?
`/images/${metadata.screenshot}` :
metadata.screenshot;
screenshotImage.src = imageSrc;
screenshotImage.alt = `${game.title} screenshot`;
screenshot.classList.remove('hidden');
} else {
screenshot.classList.add('hidden');
}
// No metadata message
const noMetadata = document.getElementById('gameNoMetadata');
const addMetadataBtn = document.getElementById('gameAddMetadataBtn');
if (!metadata.description && !metadata.year && !metadata.developer) {
noMetadata.classList.remove('hidden');
if (game.is_super) {
addMetadataBtn.classList.remove('hidden');
addMetadataBtn.onclick = () => window.open(`/admin/games/${game.id}/edit`, '_blank');
} else {
addMetadataBtn.classList.add('hidden');
}
} else {
noMetadata.classList.add('hidden');
}
}
function addInfoField(parent, label, value, colSpan = '', valueClass = 'font-medium text-primary') {
const div = document.createElement('div');
if (colSpan) div.className = colSpan;
div.innerHTML = `
<p class="text-secondary text-sm">${label}</p>
<p class="${valueClass}">${value}</p>
`;
parent.appendChild(div);
}
function closeGameDetail() {
const overlay = document.getElementById('gameDetailOverlay');
overlay.classList.add('hidden');
document.body.style.overflow = 'auto';
currentGameData = null;
}
async function toggleFavoriteInOverlay() {
if (!currentGameData) return;
try {
await toggleFavorite(currentGameData.id);
// Update the overlay button
const favoriteBtn = document.getElementById('gameFavoriteBtn');
const isFavorited = favoriteBtn.textContent.trim() === '♥';
if (isFavorited) {
favoriteBtn.textContent = '♡';
favoriteBtn.className = 'text-red-400 hover:text-red-300 text-2xl';
currentGameData.is_favorite = false;
} else {
favoriteBtn.textContent = '♥';
favoriteBtn.className = 'text-red-600 hover:text-red-300 text-2xl';
currentGameData.is_favorite = true;
}
} catch (error) {
console.error('Error toggling favorite in overlay:', error);
}
}
async function downloadGameFromOverlay() {
if (!currentGameData) return;
await downloadGame(currentGameData.id);
}
function openScreenshotModalInOverlay() {
if (!currentGameData || !currentGameData.metadata.screenshot) return;
const modal = document.getElementById('overlayScreenshotModal');
const image = document.getElementById('overlayScreenshotModalImage');
const title = document.getElementById('overlayScreenshotModalTitle');
const imageSrc = currentGameData.metadata.screenshot_local ?
`/images/${currentGameData.metadata.screenshot}` :
currentGameData.metadata.screenshot;
image.src = imageSrc;
image.alt = `${currentGameData.title} screenshot`;
title.textContent = `${currentGameData.title} - Screenshot`;
modal.classList.remove('hidden');
}
function closeOverlayScreenshotModal() {
const modal = document.getElementById('overlayScreenshotModal');
modal.classList.add('hidden');
}
// Close overlay when clicking outside
document.getElementById('gameDetailOverlay').addEventListener('click', function(e) {
if (e.target === this) {
closeGameDetail();
}
});
// Close screenshot modal when clicking outside
document.getElementById('overlayScreenshotModal').addEventListener('click', function(e) {
if (e.target === this) {
closeOverlayScreenshotModal();
}
});
// Close modals with Escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
const gameOverlay = document.getElementById('gameDetailOverlay');
const screenshotModal = document.getElementById('overlayScreenshotModal');
if (!screenshotModal.classList.contains('hidden')) {
closeOverlayScreenshotModal();
} else if (!gameOverlay.classList.contains('hidden')) {
closeGameDetail();
}
}
});
</script>
{% endblock %}