From 94968246374cc942451278ca836cd86b61ff1489 Mon Sep 17 00:00:00 2001 From: dsutanto Date: Thu, 30 Jul 2026 20:53:56 +0700 Subject: [PATCH] Folder View MP4 --- recounting_dashboard.py | 16 ++++++++-- templates/recounting.html | 62 +++++++++++++++++++++++++++++++-------- 2 files changed, 63 insertions(+), 15 deletions(-) diff --git a/recounting_dashboard.py b/recounting_dashboard.py index c3dc477..429a61a 100644 --- a/recounting_dashboard.py +++ b/recounting_dashboard.py @@ -104,18 +104,28 @@ def api_recount_progress(): @app.route("/api/mp4-files") def api_mp4_files(): - files = [] + folders = {} output = Path(OUTPUT_DIR) if output.exists(): for f in sorted(output.rglob("*.mp4"), key=lambda p: p.stat().st_mtime, reverse=True): st = f.stat() - files.append({ + rel = f.relative_to(output) + folder = str(rel.parent) if str(rel.parent) != "." else "" + if folder not in folders: + folders[folder] = [] + folders[folder].append({ "name": f.name, "path": str(f), "size": st.st_size, "mtime": datetime.fromtimestamp(st.st_mtime).isoformat(), }) - return jsonify(files) + result = [] + for folder, files in sorted(folders.items(), key=lambda x: x[0], reverse=True): + result.append({ + "folder": folder, + "files": files, + }) + return jsonify(result) @app.route("/api/start-recount", methods=["POST"]) diff --git a/templates/recounting.html b/templates/recounting.html index ac23e04..5e45751 100644 --- a/templates/recounting.html +++ b/templates/recounting.html @@ -288,6 +288,23 @@ } .file-item.streaming .fi-icon { color: var(--accent4); animation: blink 1.5s ease-in-out infinite; } + .folder-header { + display: flex; align-items: center; gap: 8px; + padding: 8px 10px; margin-bottom: 2px; + background: var(--cell-bg); + border: 1px solid var(--cell-border); border-radius: 8px; + cursor: pointer; transition: border-color 0.2s; + font-size: 10px; letter-spacing: 1px; + color: var(--accent2); + } + .folder-header:hover { border-color: var(--border-glow); } + .folder-header .folder-arrow { + font-size: 10px; transition: transform 0.2s; width: 14px; text-align: center; + } + .folder-header.open .folder-arrow { transform: rotate(90deg); } + .folder-body { display: none; padding-left: 8px; } + .folder-body.open { display: block; } + /* Buttons */ .btn { font-family: 'Orbitron', sans-serif; font-size: 10px; letter-spacing: 2px; @@ -493,23 +510,37 @@ function applyState(data) { async function loadFiles() { try { const res = await fetch('/api/mp4-files'); - const files = await res.json(); + const folders = await res.json(); const container = document.getElementById('file-list'); - if (files.length === 0) { + if (!folders || folders.length === 0) { container.innerHTML = '
No MP4 files found
'; return; } - container.innerHTML = files.map(f => { - const sel = selectedFile === f.path; - const str = streaming && selectedFile === f.path; - let cls = 'file-item'; - if (str) cls += ' streaming'; - else if (sel) cls += ' selected'; + container.innerHTML = folders.map((g, gi) => { + const label = g.folder || 'root'; + const fid = 'folder-' + gi; + const wasOpen = document.getElementById(fid) && document.getElementById(fid).classList.contains('open'); return ` -
- ${str ? '◉' : '▶'} - ${f.name} - ${formatSize(f.size)} +
+ ▶ + + ${label} + (${g.files.length}) +
+
+ ${g.files.map(f => { + const sel = selectedFile === f.path; + const str = streaming && selectedFile === f.path; + let cls = 'file-item'; + if (str) cls += ' streaming'; + else if (sel) cls += ' selected'; + return ` +
+ ${str ? '◉' : '▶'} + ${f.name} + ${formatSize(f.size)} +
`; + }).join('')}
`; }).join(''); } catch (err) { @@ -517,6 +548,13 @@ async function loadFiles() { } } +function toggleFolder(fid) { + const hdr = document.getElementById(fid + '-hdr'); + const body = document.getElementById(fid); + if (hdr) hdr.classList.toggle('open'); + if (body) body.classList.toggle('open'); +} + function selectFile(path, name) { if (selectedFile === path) { selectedFile = null;