Folder View MP4
This commit is contained in:
1 parent
5593d5e392
commit
9496824637
2 files changed
+63
-15
No files matched your search
+13
-3
@@ -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"])
|
||||
|
||||
+50
-12
@@ -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 = '<div style="font-size:10px;color:var(--text-secondary);text-align:center;padding:20px;">No MP4 files found</div>';
|
||||
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 `
|
||||
<div class="${cls}" onclick="selectFile('${f.path.replace(/'/g, "\\'")}', '${f.name.replace(/'/g, "\\'")}')">
|
||||
<span class="fi-icon">${str ? '◉' : '▶'}</span>
|
||||
<span class="fi-name">${f.name}</span>
|
||||
<span class="fi-size">${formatSize(f.size)}</span>
|
||||
<div class="folder-header" id="${fid}-hdr" onclick="toggleFolder('${fid}')">
|
||||
<span class="folder-arrow">▶</span>
|
||||
<i class="fas fa-folder" style="color:var(--accent2);"></i>
|
||||
<span>${label}</span>
|
||||
<span style="font-size:8px;color:var(--text-secondary);">(${g.files.length})</span>
|
||||
</div>
|
||||
<div class="folder-body${wasOpen ? ' open' : ''}" id="${fid}">
|
||||
${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 `
|
||||
<div class="${cls}" onclick="event.stopPropagation(); selectFile('${f.path.replace(/'/g, "\\'")}', '${f.name.replace(/'/g, "\\'")}')">
|
||||
<span class="fi-icon">${str ? '◉' : '▶'}</span>
|
||||
<span class="fi-name">${f.name}</span>
|
||||
<span class="fi-size">${formatSize(f.size)}</span>
|
||||
</div>`;
|
||||
}).join('')}
|
||||
</div>`;
|
||||
}).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;
|
||||
|
||||
Reference in new issue
Block a user