Files
MediaCollectorLibary/resources/views/admin/games/edit.twig
Lars Behrends 04140786a7 Stuff i guess ?
2025-10-31 00:24:17 +01:00

738 lines
36 KiB
Twig

{% extends 'admin/layout.twig' %}
{% block title %}{{ title }} - Admin Panel - MediaLib{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1 class="h3 mb-0">{{ title }}</h1>
<p class="text-muted mb-0">{{ game ? 'Edit game details' : 'Add a new game to your library' }}</p>
</div>
<a href="{{ path_for('admin.games.index') }}" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-2"></i>Back to Games
</a>
</div>
<div class="row">
<div class="col-lg-8">
<div class="card mb-4">
<div class="card-body">
{% if flash.getMessage('success') %}
<div class="alert alert-success">
{{ flash.getMessage('success') | first }}
</div>
{% endif %}
<form method="post" enctype="multipart/form-data">
<div class="mb-3">
<label for="title" class="form-label">Title *</label>
<input type="text" class="form-control" id="title" name="title" required
value="{{ old.title ?? game.title ?? '' }}">
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="release_date" class="form-label">Release Date</label>
<input type="date" class="form-control" id="release_date" name="release_date"
value="{{ old.release_date ?? (game.release_date ? game.release_date|date('Y-m-d') : '') }}">
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="platform" class="form-label">Platform</label>
<select class="form-select" id="platform" name="platform">
<option value="">Select Platform</option>
<option value="PC" {{ (old.platform ?? game.platform ?? '') == 'PC' ? 'selected' : '' }}>PC</option>
<option value="PlayStation" {{ (old.platform ?? game.platform ?? '') == 'PlayStation' ? 'selected' : '' }}>PlayStation</option>
<option value="Xbox" {{ (old.platform ?? game.platform ?? '') == 'Xbox' ? 'selected' : '' }}>Xbox</option>
<option value="Nintendo" {{ (old.platform ?? game.platform ?? '') == 'Nintendo' ? 'selected' : '' }}>Nintendo</option>
<option value="Mobile" {{ (old.platform ?? game.platform ?? '') == 'Mobile' ? 'selected' : '' }}>Mobile</option>
<option value="Other" {{ (old.platform ?? game.platform ?? '') == 'Other' ? 'selected' : '' }}>Other</option>
</select>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="developer" class="form-label">Developer</label>
<input type="text" class="form-control" id="developer" name="developer"
value="{{ old.developer ?? game.developer ?? '' }}">
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="publisher" class="form-label">Publisher</label>
<input type="text" class="form-control" id="publisher" name="publisher"
value="{{ old.publisher ?? game.publisher ?? '' }}">
</div>
</div>
</div>
<div class="mb-3">
<label for="genres" class="form-label">Genres (comma-separated)</label>
<input type="text" class="form-control" id="genres" name="genres"
value="{{ old.genres ?? (game.genres ? game.genres|join(', ') : '') }}">
<div class="form-text">Example: Action, Adventure, RPG</div>
</div>
<div class="mb-3">
<label for="description" class="form-label">Description</label>
<textarea class="form-control" id="description" name="description" rows="4">{{ old.description ?? game.description ?? '' }}</textarea>
</div>
<div class="mb-3">
<label for="cover_image" class="form-label">Cover Image URL</label>
<input type="text" class="form-control" id="cover_image" name="cover_image"
value="{{ old.cover_image ?? game.cover_image ?? '' }}">
</div>
<div class="mb-3">
<label for="trailer_url" class="form-label">Trailer URL</label>
<input type="text" class="form-control" id="trailer_url" name="trailer_url"
value="{{ old.trailer_url ?? game.trailer_url ?? '' }}">
</div>
<div class="d-flex justify-content-between">
<button type="submit" class="btn btn-primary">
<i class="bi bi-save me-2"></i>Save Changes
</button>
{% if game %}
<button type="button" class="btn btn-outline-danger" data-bs-toggle="modal" data-bs-target="#deleteModal">
<i class="bi bi-trash me-2"></i>Delete Game
</button>
{% endif %}
</div>
</form>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">Cover Preview</h5>
</div>
<div class="card-body text-center">
<div id="coverPreview" class="mb-3" style="min-height: 300px; display: flex; align-items: center; justify-content: center; background-color: #f8f9fa;">
{% if game and game.cover_image %}
<img src="{{ game.cover_image }}" alt="Cover" class="img-fluid" style="max-height: 300px;">
{% else %}
<div class="text-muted">No cover available</div>
{% endif %}
</div>
</div>
</div>
<!-- SteamGridDB Media -->
{% if game %}
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">SteamGridDB Media</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">Search for game media</label>
<div class="input-group">
<input type="text" id="sgdb-search" class="form-control" placeholder="Search for a game...">
<button class="btn btn-primary" type="button" id="sgdb-search-btn">
<i class="bi bi-search"></i> Search
</button>
</div>
<div id="sgdb-search-results" class="mt-3"></div>
</div>
<div id="media-selection" class="d-none">
<h6>Available Media</h6>
<div class="mb-3">
<ul id="media-tabs" class="nav nav-tabs" role="tablist"></ul>
<div id="media-content" class="tab-content p-3 border border-top-0 rounded-bottom"></div>
</div>
<div class="d-flex justify-content-between align-items-center">
<button class="btn btn-sm btn-outline-secondary" id="back-to-search">
<i class="bi bi-arrow-left me-1"></i> Back to Search
</button>
<button class="btn btn-sm btn-primary set-media-btn" disabled>
<i class="bi bi-check-circle me-1"></i> Use Selected
</button>
</div>
</div>
</div>
</div>
{% endif %}
<!-- Metadata -->
<div class="card">
<div class="card-header">
<h5 class="mb-0">Metadata</h5>
</div>
<div class="card-body">
<div class="mb-2">
<span class="text-muted">Created:</span>
<span class="float-end">{{ game ? game.created_at|date('Y-m-d H:i') : 'New' }}</span>
</div>
<div class="mb-2">
<span class="text-muted">Last Updated:</span>
<span class="float-end">{{ game ? game.updated_at|date('Y-m-d H:i') : 'N/A' }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- Delete Confirmation Modal -->
{% if game %}
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteModalLabel">Confirm Deletion</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>Are you sure you want to delete "{{ game.title }}"? This action cannot be undone.</p>
<p class="text-danger"><strong>Warning:</strong> This will remove all data associated with this game.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<form action="{{ path_for('admin.games.delete', {id: game.id}) }}" method="post" class="d-inline">
<input type="hidden" name="_METHOD" value="DELETE">
<button type="submit" class="btn btn-danger">Delete Game</button>
</form>
</div>
</div>
</div>
</div>
{% endif %}
{% endblock %}
{% block styles %}
<style>
.media-tab-pane {
display: none;
}
.media-tab-pane.active {
display: block;
}
.media-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 1rem;
margin-top: 1rem;
}
.media-item {
cursor: pointer;
border: 2px solid transparent;
border-radius: 4px;
overflow: hidden;
transition: all 0.2s;
}
.media-item:hover {
border-color: var(--bs-primary);
transform: translateY(-2px);
}
.media-item.selected {
border-color: var(--bs-primary);
box-shadow: 0 0 0 2px var(--bs-primary);
}
.media-item img {
width: 100%;
height: auto;
aspect-ratio: 1;
object-fit: cover;
}
.media-preview {
max-width: 100%;
max-height: 300px;
margin: 1rem 0;
}
</style>
{% endblock %}
{% block scripts %}
<script>
// Define all functions first
async function loadGameMedia(gameId) {
const tabs = [
{ type: 'grids', title: 'Grids', field: 'cover_url' },
{ type: 'heroes', title: 'Heroes', field: 'banner_url' },
{ type: 'icons', title: 'Icons', field: 'icon_url' },
{ type: 'logos', title: 'Logos', field: 'logo_url' }
];
// Add hidden fields if they don't exist
const form = document.querySelector('form');
['banner_url', 'icon_url', 'logo_url'].forEach(field => {
if (!form.querySelector(`[name="${field}"]`)) {
const input = document.createElement('input');
input.type = 'hidden';
input.name = field;
input.id = field;
form.appendChild(input);
}
});
const tabsContainer = document.getElementById('media-tabs');
const contentContainer = document.getElementById('media-content');
// Clear existing content
tabsContainer.innerHTML = '';
contentContainer.innerHTML = '';
// Create tabs
const tabList = document.createElement('ul');
tabList.className = 'nav nav-tabs';
tabList.role = 'tablist';
// Create tab content
tabs.forEach((tab, index) => {
// Tab button
const tabItem = document.createElement('li');
tabItem.className = 'nav-item';
tabItem.role = 'presentation';
const tabButton = document.createElement('button');
tabButton.className = `nav-link ${index === 0 ? 'active' : ''}`;
tabButton.setAttribute('data-bs-toggle', 'tab');
tabButton.setAttribute('data-bs-target', `#${tab.type}-pane`);
tabButton.type = 'button';
tabButton.role = 'tab';
tabButton.textContent = tab.title;
tabItem.appendChild(tabButton);
tabList.appendChild(tabItem);
// Tab content
const tabPane = document.createElement('div');
tabPane.id = `${tab.type}-pane`;
tabPane.className = `media-tab-pane fade ${index === 0 ? 'show active' : ''}`;
tabPane.setAttribute('data-field', tab.field);
tabPane.role = 'tabpanel';
tabPane.innerHTML = `
<div class="d-flex justify-content-between align-items-center mb-3">
<h6>Select a ${tab.title.toLowerCase()} image</h6>
<div class="spinner-border spinner-border-sm" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
<div class="media-grid" id="${tab.type}-grid"></div>
<div class="mt-3">
<img src="" class="media-preview d-none img-thumbnail mb-2" alt="Selected image">
<button type="button" class="btn btn-sm btn-primary use-selected-btn" disabled>
<i class="bi bi-check-circle me-1"></i> Use Selected
</button>
</div>
`;
contentContainer.appendChild(tabPane);
// Load media for this tab
loadMediaForTab(tab.type, tab.field, gameId);
});
tabsContainer.appendChild(tabList);
// Show the media selection section
document.getElementById('media-selection').classList.remove('d-none');
}
async function loadMediaForTab(type, field, gameId) {
const grid = document.getElementById(`${type}-grid`);
if (!grid) return;
try {
const response = await fetch(`/admin/games/sgdb/media/${gameId}/${type}`);
const data = await response.json();
if (data.success && data.data && data.data.length > 0) {
grid.innerHTML = '';
data.data.forEach((item, index) => {
const mediaItem = document.createElement('div');
mediaItem.className = 'media-item';
mediaItem.setAttribute('data-url', item.url);
mediaItem.setAttribute('data-thumb', item.thumb || item.url);
mediaItem.setAttribute('title', item.style || item.dimensions || '');
const img = document.createElement('img');
img.src = item.thumb || item.url;
img.alt = `Image ${index + 1}`;
img.className = 'img-fluid';
mediaItem.appendChild(img);
grid.appendChild(mediaItem);
// Add click handler for media selection
mediaItem.addEventListener('click', function() {
const container = this.closest('.media-tab-pane');
container.querySelectorAll('.media-item').forEach(i => i.classList.remove('selected'));
this.classList.add('selected');
const url = this.getAttribute('data-url');
const field = container.getAttribute('data-field');
const preview = container.querySelector('.media-preview');
if (preview) {
preview.src = url;
preview.classList.remove('d-none');
}
// Enable the use selected button
const useButton = container.querySelector('.use-selected-btn');
if (useButton) {
useButton.disabled = false;
useButton.onclick = function() {
// Update the corresponding form field
const field = container.getAttribute('data-field');
let input = document.querySelector(`input[name="${field}"]`);
// If input doesn't exist, create it
if (!input) {
input = document.createElement('input');
input.type = 'hidden';
input.name = field;
input.id = field;
document.querySelector('form').appendChild(input);
}
input.value = url;
// For cover_url, also update the visible input if it exists
if (field === 'cover_url') {
const coverInput = document.getElementById('cover_url');
if (coverInput) {
coverInput.value = url;
}
}
// Show success message
const alert = document.createElement('div');
alert.className = 'alert alert-success mt-2';
alert.textContent = 'Media selected successfully!';
container.querySelector('.media-preview').insertAdjacentElement('afterend', alert);
// Remove the alert after 3 seconds
setTimeout(() => alert.remove(), 3000);
};
}
});
});
} else {
grid.innerHTML = '<div class="alert alert-info">No media found for this type.</div>';
}
} catch (error) {
console.error(`Error loading ${type}:`, error);
grid.innerHTML = '<div class="alert alert-danger">Failed to load media. Please try again.</div>';
}
}
// Media tabs
document.addEventListener('DOMContentLoaded', function() {
// Handle tab switching
const tabButtons = document.querySelectorAll('[data-bs-toggle="tab"]');
// Initialize first tab as active
if (tabButtons.length > 0) {
document.querySelector('#media-tabs .nav-link').click();
}
tabButtons.forEach(button => {
button.addEventListener('click', function(e) {
e.preventDefault();
const target = this.getAttribute('data-bs-target');
document.querySelectorAll('.media-tab-pane').forEach(pane => {
pane.classList.remove('active');
});
document.querySelector(target).classList.add('active');
// Update active tab button
tabButtons.forEach(btn => btn.classList.remove('active'));
this.classList.add('active');
});
});
// Load media for a specific game
async function loadGameMedia(gameId) {
const tabs = [
{ type: 'grids', title: 'Grids', field: 'image_url' },
{ type: 'heroes', title: 'Heroes', field: 'banner_url' },
{ type: 'icons', title: 'Icons', field: 'icon' },
{ type: 'logos', title: 'Logos', field: 'logo_url' }
];
const tabsContainer = document.getElementById('media-tabs');
const contentContainer = document.getElementById('media-content');
// Clear existing content
tabsContainer.innerHTML = '';
contentContainer.innerHTML = '';
// Create tabs
const tabList = document.createElement('ul');
tabList.className = 'nav nav-tabs';
tabList.role = 'tablist';
// Create tab content
tabs.forEach((tab, index) => {
// Tab button
const tabItem = document.createElement('li');
tabItem.className = 'nav-item';
tabItem.role = 'presentation';
const tabButton = document.createElement('button');
tabButton.className = `nav-link ${index === 0 ? 'active' : ''}`;
tabButton.setAttribute('data-bs-toggle', 'tab');
tabButton.setAttribute('data-bs-target', `#${tab.type}-pane`);
tabButton.type = 'button';
tabButton.role = 'tab';
tabButton.textContent = tab.title;
tabItem.appendChild(tabButton);
tabList.appendChild(tabItem);
// Tab content
const tabPane = document.createElement('div');
tabPane.id = `${tab.type}-pane`;
tabPane.className = `media-tab-pane fade ${index === 0 ? 'show active' : ''}`;
tabPane.setAttribute('data-field', tab.field);
tabPane.role = 'tabpanel';
tabPane.innerHTML = `
<div class="d-flex justify-content-between align-items-center mb-3">
<h6>Select a ${tab.title.toLowerCase()} image</h6>
<div class="spinner-border spinner-border-sm" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
<div class="media-grid" id="${tab.type}-grid"></div>
<img src="" class="media-preview d-none img-thumbnail" alt="Selected image">
`;
contentContainer.appendChild(tabPane);
// Load media for this tab
loadMediaForTab(tab.type, tab.field, gameId);
});
tabsContainer.appendChild(tabList);
// Show the first tab
if (tabs.length > 0) {
document.querySelector('#media-tabs .nav-link').click();
}
}
// Load media for a specific tab
async function loadMediaForTab(type, field, gameId) {
const grid = document.getElementById(`${type}-grid`);
if (!grid) return;
try {
const response = await fetch(`/admin/games/sgdb/media/${gameId}/${type}`);
const data = await response.json();
if (data.success && data.data.length > 0) {
grid.innerHTML = '';
data.data.forEach((item, index) => {
const img = document.createElement('div');
img.className = 'media-item';
img.setAttribute('data-url', item.url);
img.setAttribute('data-thumb', item.thumb || item.url);
img.setAttribute('title', item.style || item.dimensions || '');
const imgElement = document.createElement('img');
imgElement.src = item.thumb || item.url;
imgElement.alt = `Image ${index + 1}`;
imgElement.loading = 'lazy';
img.appendChild(imgElement);
grid.appendChild(img);
// Add click handler to update the form field
img.addEventListener('click', function() {
const url = this.getAttribute('data-url');
const input = document.querySelector(`input[name="${field}"]`);
if (input) {
input.value = url;
}
// Show preview
const preview = this.closest('.media-tab-pane').querySelector('.media-preview');
if (preview) {
preview.src = url;
preview.classList.remove('d-none');
}
});
});
} else {
grid.innerHTML = '<div class="col-12"><p class="text-muted">No media found.</p></div>';
}
} catch (error) {
console.error(`Error loading ${type}:`, error);
grid.innerHTML = '<div class="col-12"><p class="text-danger">Error loading media. Please try again.</p></div>';
}
}
// Handle form submission for setting media
document.querySelectorAll('.set-media-btn').forEach(button => {
button.addEventListener('click', async function() {
const tabPane = this.closest('.media-tab-pane');
const selectedItem = tabPane.querySelector('.media-item.selected');
if (!selectedItem) {
alert('Please select an image first');
return;
}
const url = selectedItem.getAttribute('data-url');
const field = tabPane.getAttribute('data-field');
const gameId = '{{ game.id ?? 0 }}';
try {
const response = await fetch(`/admin/games/${gameId}/sgdb/media`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({
type: field.replace('_url', '').replace('_', '-'),
url: url
})
});
const data = await response.json();
if (data.success) {
// Update the form field
const input = document.querySelector(`input[name="${field}"]`);
if (input) {
input.value = data.data.url;
}
// Show success message
const alert = document.createElement('div');
alert.className = 'alert alert-success mt-3';
alert.textContent = 'Media updated successfully';
tabPane.insertBefore(alert, tabPane.firstChild);
// Remove the message after 3 seconds
setTimeout(() => {
alert.remove();
}, 3000);
} else {
throw new Error(data.message || 'Failed to update media');
}
} catch (error) {
console.error('Error updating media:', error);
alert('Failed to update media: ' + error.message);
}
});
});
});
// Handle SteamGridDB search
const searchInput = document.getElementById('sgdb-search');
const searchBtn = document.getElementById('sgdb-search-btn');
const searchResults = document.getElementById('sgdb-search-results');
const mediaSelection = document.getElementById('media-selection');
const backToSearch = document.getElementById('back-to-search');
if (searchBtn && searchInput) {
// Search for games
searchBtn.addEventListener('click', async () => {
const query = searchInput.value.trim();
if (!query) return;
searchBtn.disabled = true;
searchBtn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Searching...';
searchResults.innerHTML = '<div class="text-center"><div class="spinner-border text-primary" role="status"><span class="visually-hidden">Loading...</span></div><p class="mt-2">Searching for games...</p></div>';
try {
const response = await fetch(`/admin/games/sgdb/search?q=${encodeURIComponent(query)}`);
const data = await response.json();
if (data.success && data.data && data.data.length > 0) {
let html = '<div class="list-group">';
data.data.forEach(game => {
html += `
<a href="#" class="list-group-item list-group-item-action" data-game-id="${game.id}">
<div class="d-flex w-100 justify-content-between">
<h6 class="mb-1">${game.name}</h6>
<small>${game.release_date ? new Date(game.release_date * 1000).getFullYear() : 'N/A'}</small>
</div>
<p class="mb-1">${game.platforms ? game.platforms.join(', ') : 'Unknown Platform'}</p>
</a>
`;
});
html += '</div>';
searchResults.innerHTML = html;
// Add click handlers for game selection
document.querySelectorAll('#sgdb-search-results .list-group-item').forEach(item => {
item.addEventListener('click', (e) => {
e.preventDefault();
const gameId = item.getAttribute('data-game-id');
loadGameMedia(gameId);
searchResults.innerHTML = '';
mediaSelection.classList.remove('d-none');
});
});
} else {
searchResults.innerHTML = '<div class="alert alert-info">No games found. Try a different search term.</div>';
}
} catch (error) {
console.error('Error searching SteamGridDB:', error);
searchResults.innerHTML = '<div class="alert alert-danger">Error searching for games. Please try again later.</div>';
} finally {
searchBtn.disabled = false;
searchBtn.innerHTML = '<i class="bi bi-search me-1"></i> Search';
}
});
// Handle search on Enter key
searchInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
searchBtn.click();
}
});
}
// Handle back to search button
if (backToSearch) {
backToSearch.addEventListener('click', () => {
mediaSelection.classList.add('d-none');
searchResults.innerHTML = '';
searchInput.value = '';
searchInput.focus();
});
}
// Preview cover when URL changes
document.getElementById('cover_url').addEventListener('input', function() {
const preview = document.getElementById('coverPreview');
const url = this.value.trim();
if (url) {
preview.innerHTML = `<img src="${url}" alt="Cover Preview" class="img-fluid" style="max-height: 300px;">`;
} else {
preview.innerHTML = '<div class="text-muted">No cover available</div>';
}
});
// Handle form submission
document.querySelector('form').addEventListener('submit', function(e) {
const title = document.getElementById('title').value.trim();
if (!title) {
e.preventDefault();
alert('Title is required');
return false;
}
return true;
});
</script>
{% endblock %}