From show-gallery
Universal media gallery — browse images/videos from any local folder with copy-path, enlarge, and video playback. Reusable across all gen projects.
How this skill is triggered — by the user, by Claude, or both
Slash command
/show-gallery:show-galleryclaude-sonnet-4-20250514The summary Claude sees in its skill listing — used to decide when to auto-load this skill
Browse generated images & videos from any local folder. Click to enlarge, copy path to clipboard.
Browse generated images & videos from any local folder. Click to enlarge, copy path to clipboard.
พี่ระ: "โชว์รูปใน output-fal/" หรือ "show gallery D:/path/to/folder"
→ สร้าง gallery.html ใน target folder
→ preview_start server ที่ serve folder นั้น
→ navigate to /gallery.html
→ screenshot ให้พี่ระดู
ใช้ Gallery HTML Template ด้านล่าง — Write ไฟล์ gallery.html ลงใน target folder
ต้องแก้ GALLERY_ROOT ให้เป็น absolute path ของ folder (ใช้ / ไม่ใช่ \)
.claude/launch.json ว่ามี server ที่ serve folder นี้อยู่หรือยังpreview_start → preview_eval: window.location.href = '/gallery.html'preview_screenshot ให้พี่ระดูพี่ระจะ copy path จากหน้า gallery แล้ว paste มาใน chat บอกว่าจะทำอะไรกับรูป/วิดีโอนั้น
Copy ทั้ง block ด้านล่าง แล้ว Write เป็น
gallery.htmlใน target folder แก้GALLERY_ROOTบรรทัดเดียว
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<title>Gallery</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{background:#1a1a1a;color:#fff;font-family:system-ui,sans-serif;padding:12px}
.header{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;flex-wrap:wrap;gap:8px}
.header h1{font-size:14px;color:#aaa;font-weight:400}
.header .count{font-size:13px;color:#666}
.controls{display:flex;gap:6px;align-items:center}
.controls button{background:#333;border:1px solid #555;color:#ccc;padding:4px 10px;border-radius:4px;cursor:pointer;font-size:12px}
.controls button:hover{background:#444}
.controls button.active{background:#555;color:#fff;border-color:#888}
.grid{display:grid;gap:8px}
.grid.cols-2{grid-template-columns:repeat(2,1fr)}
.grid.cols-3{grid-template-columns:repeat(3,1fr)}
.grid.cols-4{grid-template-columns:repeat(4,1fr)}
.card{position:relative;border-radius:6px;overflow:hidden;background:#222;cursor:pointer}
.card:hover{outline:2px solid #ffd700}
.card img,.card video{width:100%;display:block;aspect-ratio:2/3;object-fit:cover}
.card .info{position:absolute;bottom:0;left:0;right:0;background:linear-gradient(transparent,rgba(0,0,0,.85));padding:6px 8px}
.card .fname{font-size:11px;color:#ddd;word-break:break-all}
.card .meta{font-size:10px;color:#888;margin-top:2px}
.card .copy-btn{position:absolute;top:6px;right:6px;background:rgba(0,0,0,.7);border:1px solid #555;color:#ccc;padding:3px 8px;border-radius:4px;font-size:11px;cursor:pointer;opacity:0;transition:opacity .2s}
.card:hover .copy-btn{opacity:1}
.card .copy-btn.copied{background:#2a5a2a;border-color:#4a4;color:#8f8}
.card .badge{position:absolute;top:6px;left:6px;background:rgba(0,0,0,.7);color:#ffd700;padding:2px 6px;border-radius:3px;font-size:10px;font-weight:700}
.modal{display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.95);z-index:100;justify-content:center;align-items:center;flex-direction:column;cursor:pointer}
.modal.active{display:flex}
.modal img,.modal video{max-height:85vh;max-width:95vw;object-fit:contain;border-radius:6px}
.modal .path-bar{margin-top:8px;padding:6px 14px;background:#333;border-radius:4px;font-size:12px;color:#aaa;cursor:pointer;user-select:all}
.modal .path-bar:hover{background:#444;color:#fff}
.modal .caption{margin-top:6px;font-size:11px;color:#888;max-width:80vw;text-align:center;max-height:60px;overflow:auto}
.toast{position:fixed;bottom:20px;left:50%;transform:translateX(-50%);background:#2a5a2a;color:#8f8;padding:8px 20px;border-radius:6px;font-size:13px;opacity:0;transition:opacity .3s;z-index:200;pointer-events:none}
.toast.show{opacity:1}
</style>
</head>
<body>
<div class="header">
<h1 id="title">Gallery</h1>
<span class="count" id="count"></span>
<div class="controls">
<button onclick="setCols(2)">2</button>
<button onclick="setCols(3)" class="active">3</button>
<button onclick="setCols(4)">4</button>
<span style="color:#555">|</span>
<button onclick="filterType('all')" class="active" id="btn-all">All</button>
<button onclick="filterType('image')" id="btn-image">IMG</button>
<button onclick="filterType('video')" id="btn-video">VID</button>
</div>
</div>
<div class="grid cols-3" id="grid"></div>
<div class="modal" id="modal">
<img id="modal-img" style="display:none">
<video id="modal-vid" controls style="display:none"></video>
<div class="path-bar" id="modal-path" onclick="event.stopPropagation();copyPath(this.dataset.path)"></div>
<div class="caption" id="modal-caption"></div>
</div>
<div class="toast" id="toast">Copied!</div>
<script>
// ====== CONFIG — แก้ตรงนี้ ======
const GALLERY_ROOT = 'D:/ClaudeMediaGen/output-fal';
// =================================
const IMG_EXT = /\.(png|jpg|jpeg|webp|gif)$/i;
const VID_EXT = /\.(mp4|webm|mov)$/i;
let allItems = [];
let currentFilter = 'all';
const captions = {};
async function loadGallery() {
const resp = await fetch('./');
const html = await resp.text();
const links = [...html.matchAll(/href="([^"]+)"/g)].map(m => decodeURIComponent(m[1]));
const mediaFiles = links.filter(f => IMG_EXT.test(f) || VID_EXT.test(f)).sort();
// Load .txt captions
const txtFiles = links.filter(f => f.endsWith('.txt'));
await Promise.all(txtFiles.map(async t => {
try {
const r = await fetch(t);
captions[t.replace('.txt','')] = (await r.text()).slice(0, 200);
} catch(e) {}
}));
allItems = mediaFiles.map(f => {
const isVid = VID_EXT.test(f);
const baseName = f.replace(/\.[^.]+$/, '');
return { file: f, isVideo: isVid, caption: captions[baseName] || '' };
});
renderGrid();
}
function renderGrid() {
const grid = document.getElementById('grid');
const filtered = currentFilter === 'all' ? allItems
: currentFilter === 'image' ? allItems.filter(i => !i.isVideo)
: allItems.filter(i => i.isVideo);
document.getElementById('count').textContent =
`${filtered.length}/${allItems.length} items (${allItems.filter(i=>!i.isVideo).length} img, ${allItems.filter(i=>i.isVideo).length} vid)`;
grid.innerHTML = '';
filtered.forEach((item, i) => {
const card = document.createElement('div');
card.className = 'card';
const absPath = GALLERY_ROOT + '/' + item.file;
const sizeLabel = item.isVideo ? 'VIDEO' : '';
if (item.isVideo) {
card.innerHTML = `
<video src="${item.file}" muted preload="metadata"></video>
${sizeLabel ? `<span class="badge">${sizeLabel}</span>` : ''}
<button class="copy-btn" onclick="event.stopPropagation();copyPath('${absPath}',this)">Copy Path</button>
<div class="info"><div class="fname">${item.file}</div></div>`;
} else {
card.innerHTML = `
<img src="${item.file}" loading="lazy">
<button class="copy-btn" onclick="event.stopPropagation();copyPath('${absPath}',this)">Copy Path</button>
<div class="info"><div class="fname">${item.file}</div></div>`;
}
card.onclick = () => openModal(item, absPath);
grid.appendChild(card);
});
if (filtered.length === 0) {
grid.innerHTML = '<p style="grid-column:1/-1;text-align:center;color:#666;padding:40px">No media files found</p>';
}
}
function openModal(item, absPath) {
const modal = document.getElementById('modal');
const mImg = document.getElementById('modal-img');
const mVid = document.getElementById('modal-vid');
const mPath = document.getElementById('modal-path');
const mCap = document.getElementById('modal-caption');
if (item.isVideo) {
mImg.style.display = 'none';
mVid.style.display = 'block';
mVid.src = item.file;
mVid.play();
} else {
mVid.style.display = 'none';
mVid.pause();
mImg.style.display = 'block';
mImg.src = item.file;
}
mPath.textContent = absPath;
mPath.dataset.path = absPath;
mCap.textContent = item.caption;
modal.classList.add('active');
modal.onclick = (e) => {
if (e.target === modal) { modal.classList.remove('active'); mVid.pause(); }
};
}
function copyPath(path, btn) {
navigator.clipboard.writeText(path).then(() => {
showToast('Copied: ' + path);
if (btn) { btn.textContent = 'Copied!'; btn.classList.add('copied'); setTimeout(() => { btn.textContent = 'Copy Path'; btn.classList.remove('copied'); }, 1500); }
});
}
function showToast(msg) {
const t = document.getElementById('toast');
t.textContent = msg;
t.classList.add('show');
setTimeout(() => t.classList.remove('show'), 2000);
}
function setCols(n) {
const grid = document.getElementById('grid');
grid.className = 'grid cols-' + n;
document.querySelectorAll('.controls button').forEach(b => {
if (['2','3','4'].includes(b.textContent)) b.classList.toggle('active', b.textContent == n);
});
}
function filterType(type) {
currentFilter = type;
['all','image','video'].forEach(t => {
document.getElementById('btn-'+t)?.classList.toggle('active', t === type);
});
renderGrid();
}
loadGallery();
</script>
</body>
</html>
GALLERY_ROOT ให้ตรงกับ absolute path ของ folder (forward slashes)<title> ให้สื่อกับเนื้อหาpreview_start → navigate /gallery.html → preview_screenshot ให้พี่ระดูpreview_snapshot แทน แล้วบอกพี่ระเปิดดูใน Preview panelเมื่อพี่ระบอก "browse folder" หรือ "ดู folder" โดยไม่ระบุ path:
/gen-character-image — ใช้ gallery หลัง gen character/kie-ai — ใช้ gallery หลัง gen จาก Kie.ai/fal-ai — ใช้ gallery หลัง gen จาก fal.ai/comfyui-user — ใช้ gallery หลัง gen จาก ComfyUI/image-analysis — วิเคราะห์รูปที่เลือกจาก gallerynpx claudepluginhub thepexcel/thepexcel-claude-plugins --plugin show-galleryCreates, curates, and publishes Immich albums organized by geography, theme, or custom criteria. Automates album creation from user prompts like 'create an album from my trip to Italy'.
Routes media search requests to connected sources (Salesforce CMS, Data Cloud). Prompts user to select a source before calling any search tool. Essential for teams with existing media libraries.
Generates marketing-quality screenshots of your app using Playwright at HiDPI resolution. Use for Product Hunt, social media, landing pages, or documentation.