assets: unified tile render, empty-filter placeholder

* Orphaned items now use mediaTile() instead of a hand-written triple-
  ternary, so audio and PDF files render with the same icon/player tile
  as linked items. Eliminates the code duplication that caused audio
  files to fall through to the <img> path in the orphaned section.
* applyFilter() tracks visibleCount after toggling item display. When
  the active filter hides every item (e.g. "Audio" tab with no audio
  files) but total assets exist, a type-specific placeholder is shown:
  matching icon (ph-image / ph-music-note / ph-file-pdf) + "no {type}
  assets" text. Hidden again on the next filter switch that has results.
* Section header hide logic narrowed to [data-section]:not(.gallery-item)
  so it only targets the header divs, not the items themselves.

chore: bump version to 0.9.19

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
thandClaude Opus 4.6 committed 2026-04-12 11:51:08 +02:00
1 parent bf81d046e6
commit c04eac06c4
2 files changed
+30 -11

No files matched your search

+1 -1
View File
@@ -1,7 +1,7 @@
<?php <?php
define('APP_NAME', 'hyprcrd'); define('APP_NAME', 'hyprcrd');
define('APP_VERSION', '0.9.18'); define('APP_VERSION', '0.9.19');
// Database path — writable directory, outside webroot // Database path — writable directory, outside webroot
define('DB_PATH', __DIR__ . '/data/hyprcrd.db'); define('DB_PATH', __DIR__ . '/data/hyprcrd.db');
+29 -10
View File
@@ -118,12 +118,35 @@ export async function openGallery() {
document.querySelectorAll('#gallery-tabs .gallery-tab').forEach(t => { document.querySelectorAll('#gallery-tabs .gallery-tab').forEach(t => {
t.classList.toggle('active', t.dataset.filter === filter); t.classList.toggle('active', t.dataset.filter === filter);
}); });
let visibleCount = 0;
grid.querySelectorAll('.gallery-item').forEach(item => { grid.querySelectorAll('.gallery-item').forEach(item => {
const url = item.dataset.url || ''; const url = item.dataset.url || '';
const type = fileType(url); const type = fileType(url);
const show = filter === 'all' || type === filter; const show = filter === 'all' || type === filter;
item.style.display = show ? '' : 'none'; item.style.display = show ? '' : 'none';
if (show) visibleCount++;
}); });
// Hide section headers when no items in that section are visible
grid.querySelectorAll('[data-section]:not(.gallery-item)').forEach(hdr => {
const section = hdr.dataset.section;
const hasVisible = !!grid.querySelector(`.gallery-item[data-section="${section}"]:not([style*="display: none"])`);
hdr.style.display = hasVisible ? '' : 'none';
});
// Show/hide the empty-filter placeholder
let placeholder = grid.querySelector('.gallery-filter-empty');
if (visibleCount === 0 && total > 0) {
if (!placeholder) {
placeholder = document.createElement('div');
placeholder.className = 'gallery-filter-empty col-span-full text-center py-16';
const icons = { image: 'ph-image', audio: 'ph-music-note', pdf: 'ph-file-pdf' };
const icon = icons[filter] || 'ph-folder-open';
placeholder.innerHTML = `<i class="ph ${icon} text-4xl text-t-dim mb-2 block"></i><p class="font-pixel text-[10px] text-t-dim">no ${filter} assets</p>`;
grid.appendChild(placeholder);
}
placeholder.style.display = '';
} else if (placeholder) {
placeholder.style.display = 'none';
}
}; };
document.querySelectorAll('#gallery-tabs .gallery-tab').forEach(t => { document.querySelectorAll('#gallery-tabs .gallery-tab').forEach(t => {
t.onclick = () => applyFilter(t.dataset.filter); t.onclick = () => applyFilter(t.dataset.filter);
@@ -137,15 +160,11 @@ export async function openGallery() {
let html = ''; let html = '';
if (orphaned.length) { if (orphaned.length) {
html += `<div class="col-span-full font-pixel text-[10px] text-t-danger flex items-center gap-1.5 pt-2"><i class="ph ph-warning text-xs"></i>orphaned (not used by any card)</div>`; html += `<div class="col-span-full font-pixel text-[10px] text-t-danger flex items-center gap-1.5 pt-2" data-section="orphaned"><i class="ph ph-warning text-xs"></i>orphaned (not used by any card)</div>`;
html += orphaned.map(f => ` html += orphaned.map(f => `
<div class="gallery-item group cursor-pointer" data-orphan="${f.name}" data-url="${esc(f.url)}"> <div class="gallery-item group cursor-pointer" data-orphan="${f.name}" data-url="${esc(f.url)}" data-section="orphaned">
<div class="relative aspect-square bg-t-bg border border-t-danger/30 rounded overflow-visible"> <div class="relative">
${fileType(f.url) === 'pdf' ${mediaTile(f.url, 'border-t-danger/30 opacity-60')}
? `<div class="w-full h-full flex flex-col items-center justify-center gap-1 opacity-60"><i class="ph ph-file-pdf text-3xl text-t-danger"></i><span class="font-pixel text-[8px] text-t-dim">${esc(f.name)}</span></div>`
: fileType(f.url) === 'audio'
? `<div class="w-full h-full flex items-center justify-center opacity-60"><i class="ph ph-music-note text-3xl text-t-dim"></i></div>`
: `<img src="${f.url}" class="w-full h-full object-cover opacity-60 rounded" loading="lazy">`}
<div style="position:absolute;top:4px;right:4px;display:flex;gap:4px;" class="opacity-0 group-hover:opacity-100 transition-opacity"> <div style="position:absolute;top:4px;right:4px;display:flex;gap:4px;" class="opacity-0 group-hover:opacity-100 transition-opacity">
<button class="w-6 h-6 flex items-center justify-center bg-black/80 text-t-accent rounded border-0 cursor-pointer hover:bg-t-accent hover:text-t-bg transition-colors" data-new-orphan="${f.name}" title="new card"><i class="ph ph-plus text-sm"></i></button> <button class="w-6 h-6 flex items-center justify-center bg-black/80 text-t-accent rounded border-0 cursor-pointer hover:bg-t-accent hover:text-t-bg transition-colors" data-new-orphan="${f.name}" title="new card"><i class="ph ph-plus text-sm"></i></button>
<button class="w-6 h-6 flex items-center justify-center bg-black/80 text-t-accent rounded border-0 cursor-pointer hover:bg-t-accent hover:text-t-bg transition-colors" data-new-slide="${f.name}" title="new slide (background)"><i class="ph ph-image-square text-sm"></i></button> <button class="w-6 h-6 flex items-center justify-center bg-black/80 text-t-accent rounded border-0 cursor-pointer hover:bg-t-accent hover:text-t-bg transition-colors" data-new-slide="${f.name}" title="new slide (background)"><i class="ph ph-image-square text-sm"></i></button>
@@ -157,9 +176,9 @@ export async function openGallery() {
} }
if (linked.length) { if (linked.length) {
if (orphaned.length) html += `<div class="col-span-full font-pixel text-[10px] text-t-muted flex items-center gap-1.5 pt-3"><i class="ph ph-link text-xs"></i>linked to cards</div>`; if (orphaned.length) html += `<div class="col-span-full font-pixel text-[10px] text-t-muted flex items-center gap-1.5 pt-3" data-section="linked"><i class="ph ph-link text-xs"></i>linked to cards</div>`;
html += linked.map(img => ` html += linked.map(img => `
<div class="gallery-item group relative cursor-pointer" data-card-id="${img.cardId}" data-gallery-origin="${encodeURIComponent(img.url)}" data-url="${esc(img.url)}"> <div class="gallery-item group relative cursor-pointer" data-card-id="${img.cardId}" data-gallery-origin="${encodeURIComponent(img.url)}" data-url="${esc(img.url)}" data-section="linked">
${mediaTile(img.url, 'border-t-border')} ${mediaTile(img.url, 'border-t-border')}
<div class="absolute inset-0 bg-black/60 opacity-0 group-hover:opacity-100 transition-opacity rounded flex flex-col justify-end p-2"> <div class="absolute inset-0 bg-black/60 opacity-0 group-hover:opacity-100 transition-opacity rounded flex flex-col justify-end p-2">
<div class="flex items-center gap-1.5"> <div class="flex items-center gap-1.5">