Skip to content

Instantly share code, notes, and snippets.

@ackkerman
Created December 19, 2025 04:43
Show Gist options
  • Select an option

  • Save ackkerman/3d4fcfe8dd71c8dcd1e5e6fcea9dba22 to your computer and use it in GitHub Desktop.

Select an option

Save ackkerman/3d4fcfe8dd71c8dcd1e5e6fcea9dba22 to your computer and use it in GitHub Desktop.
simple local media gallery
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<title>ローカル動画ギャラリー</title>
<style>
:root {
color-scheme: light;
}
body {
font-family: sans-serif;
margin: 16px;
background: #f7f9fb;
}
.layout {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
gap: 16px;
}
.player-container {
position: sticky;
top: 0;
z-index: 10;
padding: 8px 0 16px;
background: linear-gradient(180deg, rgba(247, 249, 251, 1) 60%, rgba(247, 249, 251, 0));
backdrop-filter: blur(4px);
flex: 0 0 auto;
}
.player-shell {
resize: both;
overflow: hidden;
width: min(70vw, 960px);
min-width: 320px;
height: 80vh;
min-height: 180px;
max-height: 80vh;
border: 1px solid #cfd8e3;
border-radius: 10px;
background: #000;
box-shadow: 0 6px 18px rgba(17, 24, 39, 0.12);
}
#player {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
}
.controls {
margin-top: 10px;
}
.loading-indicator {
display: none;
align-items: center;
gap: 8px;
font-weight: 600;
color: #1f2a44;
}
.loading-indicator[aria-busy="true"] {
display: inline-flex;
}
.spinner {
width: 14px;
height: 14px;
border: 2px solid #cfd8e3;
border-top-color: #3f6ff1;
border-radius: 50%;
animation: spin 0.9s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
#pickFolderBtn {
padding: 8px 16px;
border-radius: 6px;
border: 1px solid #2f5de0;
background: #3f6ff1;
color: #fff;
font-weight: 600;
cursor: pointer;
}
#pickFolderBtn:hover {
background: #2f5de0;
}
#gallery {
display: grid;
grid-template-columns: repeat(auto-fill, 150px);
gap: 10px;
flex: 1 1 320px;
}
#gallery.loading {
pointer-events: none;
opacity: 0.55;
filter: grayscale(0.15);
}
.thumb {
width: 150px;
cursor: pointer;
border-radius: 8px;
box-shadow: 0 3px 10px rgba(17, 24, 39, 0.14);
}
</style>
</head>
<body>
<div class="layout">
<div class="player-container">
<div class="player-shell">
<video id="player" controls></video>
</div>
<div class="controls">
<button id="pickFolderBtn">フォルダを選択</button>
<div id="loadingIndicator" class="loading-indicator" aria-live="polite" aria-busy="false" hidden>
<span class="spinner" aria-hidden="true"></span>
<span>読み込み中...</span>
</div>
</div>
</div>
<div id="gallery"></div>
</div>
<script type="module">
const pickBtn = document.getElementById("pickFolderBtn");
const gallery = document.getElementById("gallery");
const player = document.getElementById("player");
const loadingIndicator = document.getElementById("loadingIndicator");
// 再生可能な拡張子一覧
const VIDEO_EXTS = [".mp4", ".webm", ".ogg", ".mov", ".mkv"];
const setLoading = (isLoading) => {
loadingIndicator.hidden = !isLoading;
loadingIndicator.setAttribute("aria-busy", String(isLoading));
gallery.classList.toggle("loading", isLoading);
pickBtn.disabled = isLoading;
};
// フォルダ選択
pickBtn.addEventListener("click", async () => {
try {
const dirHandle = await window.showDirectoryPicker();
setLoading(true);
gallery.innerHTML = "";
for await (const entry of dirHandle.values()) {
if (entry.kind === "file") {
const name = entry.name;
const ext = name.slice(name.lastIndexOf(".")).toLowerCase();
if (VIDEO_EXTS.includes(ext)) {
const file = await entry.getFile();
const url = URL.createObjectURL(file);
// サムネイル (video tag)
const thumb = document.createElement("video");
thumb.className = "thumb";
thumb.src = url;
thumb.muted = true;
thumb.loop = true;
thumb.title = name;
thumb.addEventListener("click", () => {
player.src = url;
player.play();
});
gallery.appendChild(thumb);
}
}
}
} catch (err) {
if (err?.name !== "AbortError") {
console.error("Failed to load directory", err);
}
} finally {
setLoading(false);
}
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment