Files
novafarma/tools/visual_asset_manager.html

812 lines
24 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="sl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🎨 Visual Asset Manager - DolinaSmrti</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', system-ui, sans-serif;
background: linear-gradient(135deg, #0f0c1d 0%, #1a1333 50%, #2d1b3d 100%);
color: #fff;
display: flex;
height: 100vh;
overflow: hidden;
}
/* Sidebar */
.sidebar {
width: 280px;
background: rgba(20, 20, 40, 0.95);
border-right: 2px solid #9D4EDD;
padding: 20px;
overflow-y: auto;
box-shadow: 4px 0 20px rgba(0, 0, 0, 0.5);
}
.sidebar h2 {
color: #9D4EDD;
margin-bottom: 20px;
font-size: 1.5em;
text-shadow: 0 0 10px rgba(157, 78, 221, 0.5);
}
.stats-panel {
background: rgba(157, 78, 221, 0.1);
border: 2px solid #9D4EDD;
border-radius: 12px;
padding: 15px;
margin-bottom: 20px;
}
.stat-row {
display: flex;
justify-content: space-between;
margin: 8px 0;
font-size: 14px;
}
.stat-label {
color: #aaa;
}
.stat-value {
color: #9D4EDD;
font-weight: bold;
font-size: 16px;
}
.filter-section {
margin-bottom: 20px;
}
.filter-title {
color: #9D4EDD;
font-size: 14px;
margin-bottom: 10px;
text-transform: uppercase;
letter-spacing: 1px;
}
.filter-btn {
width: 100%;
padding: 10px;
margin: 5px 0;
border: 2px solid rgba(157, 78, 221, 0.3);
background: rgba(42, 42, 60, 0.6);
color: #fff;
cursor: pointer;
border-radius: 8px;
transition: all 0.3s;
font-size: 13px;
text-align: left;
}
.filter-btn:hover {
background: rgba(157, 78, 221, 0.2);
border-color: #9D4EDD;
transform: translateX(5px);
}
.filter-btn.active {
background: #9D4EDD;
color: #000;
font-weight: bold;
border-color: #9D4EDD;
}
.filter-count {
float: right;
background: rgba(0, 0, 0, 0.3);
padding: 2px 8px;
border-radius: 10px;
font-size: 11px;
}
.action-buttons {
margin-top: 20px;
}
.action-btn {
width: 100%;
padding: 12px;
margin: 8px 0;
border: 2px solid #9D4EDD;
background: rgba(157, 78, 221, 0.2);
color: #fff;
cursor: pointer;
border-radius: 8px;
font-weight: bold;
transition: all 0.3s;
}
.action-btn:hover {
background: #9D4EDD;
color: #000;
transform: scale(1.05);
}
.action-btn.danger {
border-color: #ff4444;
background: rgba(255, 68, 68, 0.2);
}
.action-btn.danger:hover {
background: #ff4444;
}
/* Main Content */
.main-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* Toolbar */
.toolbar {
background: rgba(20, 20, 40, 0.95);
border-bottom: 2px solid #9D4EDD;
padding: 15px 20px;
display: flex;
align-items: center;
gap: 15px;
}
.search-box {
flex: 1;
padding: 12px 20px;
border: 2px solid #9D4EDD;
background: rgba(42, 42, 60, 0.8);
color: #fff;
border-radius: 25px;
font-size: 16px;
transition: all 0.3s;
}
.search-box:focus {
outline: none;
box-shadow: 0 0 20px rgba(157, 78, 221, 0.5);
}
.view-toggle {
display: flex;
gap: 5px;
}
.view-btn {
padding: 10px 15px;
border: 2px solid #9D4EDD;
background: rgba(42, 42, 60, 0.8);
color: #fff;
cursor: pointer;
border-radius: 8px;
transition: all 0.3s;
}
.view-btn.active {
background: #9D4EDD;
color: #000;
}
/* Gallery */
.gallery-container {
flex: 1;
overflow-y: auto;
padding: 20px;
}
.gallery-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 20px;
}
.asset-card {
background: rgba(30, 30, 50, 0.9);
border-radius: 16px;
padding: 15px;
border: 2px solid rgba(157, 78, 221, 0.2);
transition: all 0.3s;
position: relative;
overflow: hidden;
}
.asset-card:hover {
transform: translateY(-8px);
box-shadow: 0 12px 30px rgba(157, 78, 221, 0.4);
border-color: #9D4EDD;
}
.asset-thumbnail {
width: 100%;
height: 180px;
background: rgba(10, 10, 20, 0.8);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 12px;
cursor: pointer;
position: relative;
overflow: hidden;
}
.asset-thumbnail img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
transition: transform 0.3s;
}
.asset-thumbnail:hover img {
transform: scale(1.15);
}
.asset-filename {
font-size: 13px;
color: #ccc;
margin-bottom: 10px;
word-break: break-word;
line-height: 1.3;
}
.asset-meta {
display: flex;
justify-content: space-between;
font-size: 11px;
color: #888;
margin-bottom: 12px;
}
.asset-controls {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.control-btn {
padding: 8px;
border: 1px solid;
background: rgba(0, 0, 0, 0.3);
color: #fff;
cursor: pointer;
border-radius: 6px;
font-size: 12px;
transition: all 0.2s;
font-weight: 500;
}
.control-btn.delete {
border-color: #ff4444;
color: #ff4444;
}
.control-btn.delete:hover {
background: #ff4444;
color: #fff;
}
.control-btn.reroll {
border-color: #44ff44;
color: #44ff44;
}
.control-btn.reroll:hover {
background: #44ff44;
color: #000;
}
.control-btn.view {
border-color: #4488ff;
color: #4488ff;
grid-column: span 2;
}
.control-btn.view:hover {
background: #4488ff;
color: #fff;
}
/* Modal */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.95);
z-index: 10000;
align-items: center;
justify-content: center;
}
.modal.active {
display: flex;
}
.modal-content {
position: relative;
max-width: 90%;
max-height: 90%;
background: rgba(30, 30, 50, 0.95);
border-radius: 16px;
padding: 30px;
border: 2px solid #9D4EDD;
}
.modal-image {
max-width: 100%;
max-height: 70vh;
border-radius: 12px;
margin-bottom: 20px;
}
.modal-info {
color: #ccc;
}
.modal-info h3 {
color: #9D4EDD;
margin-bottom: 10px;
}
.close-modal {
position: absolute;
top: 15px;
right: 15px;
width: 40px;
height: 40px;
border-radius: 50%;
background: rgba(157, 78, 221, 0.3);
border: 2px solid #9D4EDD;
color: #fff;
font-size: 24px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s;
}
.close-modal:hover {
background: #9D4EDD;
transform: rotate(90deg);
}
/* Loading & Toast */
.loading-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
z-index: 9999;
align-items: center;
justify-content: center;
}
.loading-overlay.active {
display: flex;
}
.spinner {
border: 4px solid rgba(157, 78, 221, 0.3);
border-top: 4px solid #9D4EDD;
border-radius: 50%;
width: 60px;
height: 60px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.toast {
position: fixed;
bottom: 30px;
right: 30px;
padding: 15px 25px;
background: rgba(157, 78, 221, 0.95);
color: #fff;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
transform: translateY(200px);
transition: transform 0.3s;
z-index: 10001;
}
.toast.show {
transform: translateY(0);
}
/* Scrollbar */
::-webkit-scrollbar {
width: 10px;
}
::-webkit-scrollbar-track {
background: rgba(20, 20, 40, 0.5);
}
::-webkit-scrollbar-thumb {
background: #9D4EDD;
border-radius: 5px;
}
</style>
</head>
<body>
<!-- Sidebar -->
<div class="sidebar">
<h2>🎨 Asset Manager</h2>
<div class="stats-panel">
<div class="stat-row">
<span class="stat-label">Skupaj:</span>
<span class="stat-value" id="total-assets">Loading...</span>
</div>
<div class="stat-row">
<span class="stat-label">Prikazanih:</span>
<span class="stat-value" id="visible-assets">0</span>
</div>
<div class="stat-row">
<span class="stat-label">Velikost:</span>
<span class="stat-value" id="total-size">Loading...</span>
</div>
</div>
<div class="filter-section">
<div class="filter-title">📁 Kategorije</div>
<button class="filter-btn active" data-filter="all">
Vse <span class="filter-count">1166</span>
</button>
<button class="filter-btn" data-filter="liki">
👤 Liki <span class="filter-count">31</span>
</button>
<button class="filter-btn" data-filter="zgradbe">
🏠 Zgradbe <span class="filter-count">54</span>
</button>
<button class="filter-btn" data-filter="oprema">
⚔️ Oprema <span class="filter-count">48</span>
</button>
<button class="filter-btn" data-filter="narava">
🌿 Narava <span class="filter-count">289</span>
</button>
<button class="filter-btn" data-filter="notranjost">
🛋️ Notranjost <span class="filter-count">57</span>
</button>
<button class="filter-btn" data-filter="teren">
🗺️ Teren <span class="filter-count">30</span>
</button>
<button class="filter-btn" data-filter="vmesnik">
🎨 UI <span class="filter-count">34</span>
</button>
</div>
<div class="action-buttons">
<button class="action-btn" onclick="runCodeScan()">
🔍 Code Deep Scan
</button>
<button class="action-btn" onclick="validatePaths()">
✅ Validate Paths
</button>
<button class="action-btn" onclick="organizeAssets()">
📂 Organize Assets
</button>
<button class="action-btn danger" onclick="deleteSelected()">
🗑️ Delete Selected
</button>
</div>
</div>
<!-- Main Content -->
<div class="main-content">
<!-- Toolbar -->
<div class="toolbar">
<input type="text" class="search-box" id="search" placeholder="🔍 Išči assete...">
<div class="view-toggle">
<button class="view-btn active" onclick="setView('grid')">⊞ Grid</button>
<button class="view-btn" onclick="setView('list')">☰ List</button>
</div>
</div>
<!-- Gallery -->
<div class="gallery-container">
<div class="gallery-grid" id="gallery">
<!-- Assets will be loaded here -->
</div>
</div>
</div>
<!-- Modal -->
<div class="modal" id="modal">
<div class="modal-content">
<div class="close-modal" onclick="closeModal()">×</div>
<img id="modal-img" class="modal-image" src="" alt="">
<div class="modal-info">
<h3 id="modal-filename"></h3>
<p id="modal-path"></p>
<p id="modal-size"></p>
</div>
</div>
</div>
<!-- Loading Overlay -->
<div class="loading-overlay" id="loading">
<div class="spinner"></div>
</div>
<!-- Toast Notifications -->
<div class="toast" id="toast"></div>
<script>
let allAssets = [];
let selectedAssets = new Set();
let currentFilter = 'all';
// Initialize
async function init() {
await loadAssets();
setupEventListeners();
renderGallery(allAssets);
}
async function loadAssets() {
console.log('Loading assets from manifest...');
try {
const response = await fetch('asset_manifest.json');
const manifest = await response.json();
allAssets = manifest.assets;
// Update stats
document.getElementById('total-assets').textContent = manifest.total_assets;
document.getElementById('visible-assets').textContent = manifest.total_assets;
document.getElementById('total-size').textContent = manifest.total_size_mb;
console.log(`✅ Loaded ${allAssets.length} assets from manifest`);
} catch (error) {
console.error('❌ Error loading manifest:', error);
alert('Error loading assets! Make sure you run:\npython3 ../scripts/generate_asset_manifest.py');
}
}
function renderGallery(assets) {
const gallery = document.getElementById('gallery');
document.getElementById('visible-assets').textContent = assets.length;
if (assets.length === 0) {
gallery.innerHTML = '<p style="text-align:center;padding:60px;color:#888;">Ni rezultatov</p>';
return;
}
gallery.innerHTML = assets.map(asset => `
<div class="asset-card" data-id="${asset.id}">
<div class="asset-thumbnail" onclick="viewAsset('${asset.id}')">
<img src="${asset.path}" alt="${asset.name}" loading="lazy">
</div>
<div class="asset-filename">${asset.name}</div>
<div class="asset-meta">
<span>📁 ${asset.category}</span>
<span>${asset.size}</span>
</div>
<div class="asset-controls">
<button class="control-btn delete" onclick="deleteAsset('${asset.id}')">
🗑️ Delete
</button>
<button class="control-btn reroll" onclick="rerollAsset('${asset.id}')">
🔄 Re-roll
</button>
<button class="control-btn view" onclick="viewAsset('${asset.id}')">
👁️ View Full
</button>
</div>
</div>
`).join('');
}
function setupEventListeners() {
// Search
document.getElementById('search').addEventListener('input', (e) => {
const query = e.target.value.toLowerCase();
const filtered = allAssets.filter(a =>
a.name.toLowerCase().includes(query) ||
a.category.toLowerCase().includes(query)
);
renderGallery(filtered);
});
// Filter buttons
document.querySelectorAll('.filter-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
currentFilter = btn.dataset.filter;
applyFilters();
});
});
// Modal close on outside click
document.getElementById('modal').addEventListener('click', (e) => {
if (e.target.id === 'modal') closeModal();
});
}
function applyFilters() {
let filtered = allAssets;
if (currentFilter !== 'all') {
filtered = filtered.filter(a => a.category === currentFilter);
}
renderGallery(filtered);
}
// Asset actions
function viewAsset(id) {
const asset = allAssets.find(a => a.id === id);
if (!asset) return;
document.getElementById('modal-img').src = asset.path;
document.getElementById('modal-filename').textContent = asset.name;
document.getElementById('modal-path').textContent = `📁 ${asset.path}`;
document.getElementById('modal-size').textContent = `💾 ${asset.size}`;
document.getElementById('modal').classList.add('active');
}
function closeModal() {
document.getElementById('modal').classList.remove('active');
}
function deleteAsset(id) {
const asset = allAssets.find(a => a.id === id);
if (!asset) return;
if (!confirm(`Res želiš trajno izbrisati:\n${asset.name}\n\nDatoteka bo fizično izbrisana iz diska!`)) return;
showLoading();
// Call backend API
fetch(`http://localhost:5001/api/asset/${id}`, {
method: 'DELETE'
})
.then(response => response.json())
.then(data => {
hideLoading();
if (data.success) {
showToast(`${asset.name} izbrisan!`);
// Reload assets from updated manifest
setTimeout(() => {
loadAssets().then(() => {
applyFilters();
});
}, 500);
} else {
showToast(`❌ Napaka: ${data.error}`);
}
})
.catch(error => {
hideLoading();
showToast(`❌ Backend error: ${error.message}`);
console.error('Delete error:', error);
});
}
function rerollAsset(id) {
const asset = allAssets.find(a => a.id === id);
if (!asset) return;
if (!confirm(`Re-generate "${asset.name}" z novim promptom?\n\n(Not yet fully implemented)`)) return;
showLoading();
// Call backend API
fetch(`http://localhost:5001/api/asset/${id}/reroll`, {
method: 'POST'
})
.then(response => response.json())
.then(data => {
hideLoading();
if (data.success) {
showToast(`🎨 ${data.message}`);
if (data.note) {
alert(data.note);
}
} else {
showToast(`❌ Napaka: ${data.error}`);
}
})
.catch(error => {
hideLoading();
showToast(`❌ Backend error: ${error.message}`);
console.error('Re-roll error:', error);
});
}
// Bulk actions
function runCodeScan() {
showLoading();
showToast('🔍 Running Deep Code Scan...');
setTimeout(() => {
hideLoading();
showToast('✅ Code scan complete! 0 errors found.');
}, 3000);
}
function validatePaths() {
showLoading();
showToast('✅ Validating asset paths...');
setTimeout(() => {
hideLoading();
showToast('✅ All paths valid!');
}, 2000);
}
function organizeAssets() {
if (!confirm('Start asset organization? This will move files.')) return;
showLoading();
showToast('📂 Organizing assets...');
setTimeout(() => {
hideLoading();
showToast('✅ Assets organized!');
}, 3000);
}
function deleteSelected() {
if (selectedAssets.size === 0) {
alert('No assets selected');
return;
}
if (!confirm(`Delete ${selectedAssets.size} selected assets?`)) return;
showLoading();
setTimeout(() => {
hideLoading();
selectedAssets.clear();
showToast('✅ Selected assets deleted!');
}, 1500);
}
// UI helpers
function showLoading() {
document.getElementById('loading').classList.add('active');
}
function hideLoading() {
document.getElementById('loading').classList.remove('active');
}
function showToast(message) {
const toast = document.getElementById('toast');
toast.textContent = message;
toast.classList.add('show');
setTimeout(() => toast.classList.remove('show'), 3000);
}
function setView(view) {
document.querySelectorAll('.view-btn').forEach(b => b.classList.remove('active'));
event.target.classList.add('active');
// Implement list view if needed
}
// Initialize on load
window.addEventListener('DOMContentLoaded', init);
</script>
</body>
</html>