Move admin nav to fixed footer

This commit is contained in:
hbrain 2026-05-27 22:33:08 +02:00
parent e876d3c63a
commit 80dd447740
2 changed files with 136 additions and 21 deletions

142
new.php
View file

@ -305,8 +305,11 @@ try {
set_input_dir_enabled($dir, false);
set_input_dir_preview($dir, false);
set_input_dir_visible($dir, false);
$msg = "Created movmaker input directory: in-dir/$slug";
$_GET['edit'] = $slug;
if (!empty($_POST['ajax'])) {
ajax_json(['ok'=>true, 'message'=>"Created movmaker input directory: in-dir/$slug", 'dir'=>$slug, 'tab'=>'edit']);
}
header('Location: new.php?tab=edit');
exit;
} elseif ($action === 'set_enabled') {
$dir = safe_input_dir($config['uploads_dir'], $_POST['dir'] ?? '');
$enabledNow = !empty($_POST['enabled']);
@ -347,7 +350,8 @@ try {
write_data($dir, $_POST);
save_uploads('media', $dir, $allowedMedia);
save_uploads('audio', $dir, $allowedAudio);
header('Location: new.php?tab=edit&msg=' . rawurlencode('Updated input directory: in-dir/' . basename($dir)));
if (!empty($_POST['ajax'])) ajax_json(['ok'=>true, 'message'=>'Updated input directory: in-dir/' . basename($dir), 'dir'=>basename($dir)]);
header('Location: new.php?tab=edit');
exit;
} elseif ($action === 'delete_file') {
$dir = safe_input_dir($config['uploads_dir'], $_POST['dir'] ?? '');
@ -355,8 +359,11 @@ try {
$file = basename((string)($_POST['file'] ?? ''));
if (!editable_file($file) || !is_file($dir.'/'.$file)) throw new RuntimeException('Invalid file.');
unlink($dir.'/'.$file);
$msg = 'Removed file: ' . $file;
$_GET['edit'] = basename($dir);
if (!empty($_POST['ajax'])) {
ajax_json(['ok'=>true, 'message'=>'Removed file: ' . $file, 'tab'=>'edit']);
}
header('Location: new.php?tab=edit');
exit;
} elseif ($action === 'delete_video') {
$file = basename((string)($_POST['video'] ?? ''));
$path = $config['videos_dir'] . '/' . $file;
@ -364,7 +371,11 @@ try {
unlink($path);
$cache = __DIR__ . '/cache/videos.json';
if (is_file($cache)) unlink($cache);
$msg = 'Deleted video: ' . $file;
if (!empty($_POST['ajax'])) {
ajax_json(['ok'=>true, 'message'=>'Deleted video: ' . $file, 'tab'=>'videos']);
}
header('Location: new.php?tab=videos');
exit;
} elseif ($action === 'delete_dir') {
$dir = safe_input_dir($config['uploads_dir'], $_POST['dir'] ?? '');
if (input_dir_running($dir)) throw new RuntimeException('This input directory is being rendered. Editing is disabled until the job completes.');
@ -379,7 +390,10 @@ try {
if (is_file($cache)) unlink($cache);
}
rrmdir($dir);
header('Location: new.php?msg=' . rawurlencode('Deleted input directory: in-dir/' . $name));
if (!empty($_POST['ajax'])) {
ajax_json(['ok'=>true, 'message'=>'Deleted input directory: in-dir/' . $name, 'tab'=>'edit']);
}
header('Location: new.php?tab=edit');
exit;
}
}
@ -403,14 +417,15 @@ $editDir = null; $editStatus = ''; $editRunning = false; $editData = ['title'=>'
if ($editName !== '') { try { $editDir = safe_input_dir($config['uploads_dir'], $editName); $editStatus = input_dir_status($editDir); $editRunning = input_dir_running($editDir); if (!$editRunning) { $editData = read_data($editDir); $editFiles = media_sort_files($editDir, list_files($editDir)); } } catch (Throwable $e) { $err = $e->getMessage(); } }
?>
<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Admin - <?=h($config['site_name'])?></title><link rel="icon" type="image/png" href="assets/img/moto_travel.png"><link rel="stylesheet" href="style.css"></head><body>
<header class="site-header admin-header"><div class="admin-left"><nav class="tabs"><a class="<?= $tab==='new'?'active':'' ?>" href="new.php?tab=new">New</a><a class="<?= $tab==='edit'?'active':'' ?>" href="new.php?tab=edit">Edit</a><a class="<?= $tab==='videos'?'active':'' ?>" href="new.php?tab=videos">Videos</a></nav></div><div class="brand-wrap"><a class="header-logo" href="index.php" aria-label="MVLog home"><img src="assets/img/moto_travel.png" alt=""></a><a class="brand" href="index.php"><h1>MVLog Admin</h1><p>Bubulescu.Org</p></a></div><div class="admin-right"><div id="job-status" class="job-status" hidden></div><a class="logout-link" href="logout.php">Log off</a></div></header><main>
<?php if($msg): ?><div class="ok"><?=h($msg)?></div><?php endif; ?><?php if($err): ?><div class="err"><?=h($err)?></div><?php endif; ?>
<header class="site-header admin-header"><div class="brand-wrap"><a class="header-logo" href="index.php" aria-label="MVLog home"><img src="assets/img/moto_travel.png" alt=""></a><a class="brand" href="index.php"><h1>MVLog Admin</h1><p>Bubulescu.Org</p></a></div></header>
<main>
<?php if($err): ?><div class="err"><?=h($err)?></div><?php endif; ?>
<?php if($tab==='videos'): ?><section><h2>Videos without input dir</h2><?php if(!$orphanVideos): ?><p>No orphan videos.</p><?php endif; ?><div class="admin-list"><?php foreach($orphanVideosPage as $v): $vn=basename($v); ?><form method="post" class="admin-item" onsubmit="return confirm('Delete this generated video?')"><div><strong><?=h($vn)?></strong><br><span><?=h(round(filesize($v)/1048576,1))?> MB</span></div><input type="hidden" name="action" value="delete_video"><input type="hidden" name="video" value="<?=h($vn)?>"><button type="submit">Delete video</button></form><?php endforeach; ?></div><?=page_links('videos',$videoPage,$videoPages)?></section><?php endif; ?>
<?php if($tab==='edit'): ?><section><h2>Existing input dirs</h2><?php if(!$dirs): ?><p>No input directories yet.</p><?php endif; ?><div class="admin-list video-list"><?php foreach($dirsPage as $d): $runStatus=input_dir_status($d); $running=input_dir_running($d); $enabled=input_dir_enabled($d); $preview=input_dir_preview($d); $state=input_dir_state($d); $fullOutput=basename((string)($state['output'] ?? '')); $previewOutput=basename((string)($state['preview_output'] ?? '')); $hasFullVideo=$fullOutput !== '' && is_file($config['videos_dir'].'/'.$fullOutput); $hasPreviewVideo=$previewOutput !== '' && is_file($config['videos_dir'].'/'.$previewOutput); $displayOutput=($preview && $hasPreviewVideo) ? $previewOutput : ($hasFullVideo ? $fullOutput : ($hasPreviewVideo ? $previewOutput : '')); $hasVideo=$displayOutput !== ''; $displayVideoPath=$hasVideo ? $config['videos_dir'].'/'.$displayOutput : ''; $previewVideo=$hasVideo && $displayOutput === $previewOutput; $canShow=$hasFullVideo && !$preview; $visible=$canShow && input_dir_visible($d); $info=$dirInfo[basename($d)] ?? input_dir_info($d); $data=read_data($d); $meta=cached_video_metadata($displayOutput, $data); ?><article class="video-row admin-video-row"><div><?php if($hasVideo): ?><video controls controlsList="nodownload" oncontextmenu="return false" preload="metadata" src="<?=h($config['public_videos'].'/'.rawurlencode($displayOutput))?>"></video><?php else: ?><div class="video-placeholder"><img src="assets/img/moto_travel.png" alt=""><span>No video yet</span></div><?php endif; ?></div><div class="video-info"><div class="admin-row-top"><div class="admin-switches"><form method="post" class="inline-form"><input type="hidden" name="action" value="set_preview"><input type="hidden" name="dir" value="<?=h(basename($d))?>"><label class="switch-label"><input type="checkbox" name="preview" value="1" <?=$preview?'checked':''?>><span class="switch-ui" aria-hidden="true"></span><span class="switch-text">Preview</span></label></form><form method="post" class="inline-form"><input type="hidden" name="action" value="set_enabled"><input type="hidden" name="dir" value="<?=h(basename($d))?>"><label class="switch-label"><input type="checkbox" name="enabled" value="1" <?=$enabled?'checked':''?>><span class="switch-ui" aria-hidden="true"></span><span class="switch-text">Run</span></label></form><form method="post" class="inline-form"><input type="hidden" name="action" value="set_visible"><input type="hidden" name="dir" value="<?=h(basename($d))?>"><label class="switch-label"><input type="checkbox" name="visible" value="1" <?=$visible?'checked':''?> <?=!$canShow?'disabled':''?>><span class="switch-ui" aria-hidden="true"></span><span class="switch-text">Show</span></label></form><?php if($hasVideo): ?><time class="admin-time" title="Video created at"><?=h(date('d.m.Y H:i', filemtime($displayVideoPath)))?></time><?php endif; ?></div><div class="admin-actions"><?php if($running): ?><span class="button disabled" title="Rendering now" data-edit-action="1">Rendering</span><?php else: ?><a class="button" href="?edit=<?=rawurlencode(basename($d))?>" data-edit-action="1" data-edit-href="?edit=<?=rawurlencode(basename($d))?>">Edit</a><?php endif; ?></div></div><h2><?=h($meta['title'] ?: ($info['title'] ?? basename($d)))?></h2><p class="meta"><?php if($meta['date']): ?><span><?=h($meta['date'])?></span><?php endif; ?><?php if($meta['location']): ?><span><?=h($meta['location'])?></span><?php endif; ?><?php if(!$hasVideo): ?><span>No video</span><?php endif; ?><?php if($previewVideo): ?><span>Preview video</span><?php endif; ?></p><?php if($meta['description']): ?><p class="description"><?=nl2br(h($meta['description']), false)?></p><?php endif; ?><p class="details"><?=h(basename($d))?> · <?=h($info['file_count'] ?? count(list_files($d)))?> files · <?= $enabled ? 'Rendering enabled' : 'Rendering disabled' ?> · <?= $preview ? 'Preview enabled' : 'Preview disabled' ?> · <?= $visible ? 'Shown' : 'Hidden' ?><?= $running ? ' · '.h(ucfirst($runStatus)) : '' ?></p></div></article><?php endforeach; ?></div><?=page_links('edit',$editPage,$editPages)?></section><?php endif; ?>
<?php if($tab==='videos'): ?><section><h2>Videos without input dir</h2><?php if(!$orphanVideos): ?><p>No orphan videos.</p><?php endif; ?><div class="admin-list"><?php foreach($orphanVideosPage as $v): $vn=basename($v); ?><form method="post" class="admin-item" onsubmit="return confirm('Delete this generated video?') js-ajax-form"><div><strong><?=h($vn)?></strong><br><span><?=h(round(filesize($v)/1048576,1))?> MB</span></div><input type="hidden" name="action" value="delete_video"><input type="hidden" name="video" value="<?=h($vn)?>"><button type="submit">Delete video</button></form><?php endforeach; ?></div><?=page_links('videos',$videoPage,$videoPages)?></section><?php endif; ?>
<?php if($tab==='edit' && !$editDir): ?><section><h2>Existing input dirs</h2><?php if(!$dirs): ?><p>No input directories yet.</p><?php endif; ?><div class="admin-list video-list"><?php foreach($dirsPage as $d): $runStatus=input_dir_status($d); $running=input_dir_running($d); $enabled=input_dir_enabled($d); $preview=input_dir_preview($d); $state=input_dir_state($d); $fullOutput=basename((string)($state['output'] ?? '')); $previewOutput=basename((string)($state['preview_output'] ?? '')); $hasFullVideo=$fullOutput !== '' && is_file($config['videos_dir'].'/'.$fullOutput); $hasPreviewVideo=$previewOutput !== '' && is_file($config['videos_dir'].'/'.$previewOutput); $displayOutput=($preview && $hasPreviewVideo) ? $previewOutput : ($hasFullVideo ? $fullOutput : ($hasPreviewVideo ? $previewOutput : '')); $hasVideo=$displayOutput !== ''; $displayVideoPath=$hasVideo ? $config['videos_dir'].'/'.$displayOutput : ''; $previewVideo=$hasVideo && $displayOutput === $previewOutput; $canShow=$hasFullVideo && !$preview; $visible=$canShow && input_dir_visible($d); $info=$dirInfo[basename($d)] ?? input_dir_info($d); $data=read_data($d); $meta=cached_video_metadata($displayOutput, $data); ?><article class="video-row admin-video-row<?= $visible ? ' shown' : '' ?>"><div><?php if($hasVideo): ?><video controls controlsList="nodownload" oncontextmenu="return false" preload="metadata" src="<?=h($config['public_videos'].'/'.rawurlencode($displayOutput))?>"></video><?php else: ?><div class="video-placeholder"><img src="assets/img/moto_travel.png" alt=""><span>No video yet</span></div><?php endif; ?></div><div class="video-info"><div class="admin-row-top"><div class="admin-switches"><form method="post" class="inline-form"><input type="hidden" name="action" value="set_preview"><input type="hidden" name="dir" value="<?=h(basename($d))?>"><label class="switch-label"><input type="checkbox" name="preview" value="1" <?=$preview?'checked':''?>><span class="switch-ui" aria-hidden="true"></span><span class="switch-text">Preview</span></label></form><form method="post" class="inline-form"><input type="hidden" name="action" value="set_enabled"><input type="hidden" name="dir" value="<?=h(basename($d))?>"><label class="switch-label"><input type="checkbox" name="enabled" value="1" <?=$enabled?'checked':''?>><span class="switch-ui" aria-hidden="true"></span><span class="switch-text">Render</span></label></form><form method="post" class="inline-form"><input type="hidden" name="action" value="set_visible"><input type="hidden" name="dir" value="<?=h(basename($d))?>"><label class="switch-label"><input type="checkbox" name="visible" value="1" <?=$visible?'checked':''?> <?=!$canShow?'disabled':''?>><span class="switch-ui" aria-hidden="true"></span><span class="switch-text">Show</span></label></form><?php if($hasVideo): ?><time class="admin-time" title="Video created at"><?=h(date('d.m.Y H:i', filemtime($displayVideoPath)))?></time><?php endif; ?></div><div class="admin-actions"><?php if($running): ?><span class="button disabled" title="Rendering now" data-edit-action="1">Rendering</span><?php else: ?><a class="button" href="?edit=<?=rawurlencode(basename($d))?>" data-edit-action="1" data-edit-href="?edit=<?=rawurlencode(basename($d))?>">Edit</a><?php endif; ?></div></div><h2><?=h($meta['title'] ?: ($info['title'] ?? basename($d)))?></h2><p class="meta"><?php if($meta['date']): ?><span><?=h($meta['date'])?></span><?php endif; ?><?php if($meta['location']): ?><span><?=h($meta['location'])?></span><?php endif; ?><?php if(!$hasVideo): ?><span>No video</span><?php endif; ?><?php if($previewVideo): ?><span>Preview video</span><?php endif; ?></p><?php if($meta['description']): ?><p class="description"><?=nl2br(h($meta['description']), false)?></p><?php endif; ?><p class="details"><?=h(basename($d))?> · <?=h($info['file_count'] ?? count(list_files($d)))?> files</p></div></article><?php endforeach; ?></div><?=page_links('edit',$editPage,$editPages)?></section><?php endif; ?>
<?php if($tab==='edit' && $editDir && $editRunning): ?><section><h2>Edit input dir</h2><p><code>in-dir/<?=h(basename($editDir))?></code></p><div class="err">This input directory is <?=h($editStatus)?>. Editing is disabled until the job completes.</div></section><?php endif; ?>
<?php if($tab==='edit' && $editDir && !$editRunning): ?><section><h2>Edit input dir</h2><p><code>in-dir/<?=h(basename($editDir))?></code></p><form method="post" enctype="multipart/form-data"><input type="hidden" name="action" value="update"><input type="hidden" name="dir" value="<?=h(basename($editDir))?>"><label>Title<input name="title" value="<?=h($editData['title'])?>"></label><label>Date<input name="date" value="<?=h($editData['date'])?>"></label><label>Place<input name="place" value="<?=h($editData['place'])?>"></label><label>Description<textarea name="description" maxlength="5000" data-counter="description-counter-edit"><?=h($editData['description'])?></textarea><small id="description-counter-edit" class="counter"></small></label><label>Add images / videos<input type="file" name="media[]" multiple accept="image/*,video/*"></label><label>Add audio<input type="file" name="audio[]" multiple accept="audio/*"></label><h3>Files and captions</h3><div class="file-list"><?php foreach($editFiles as $f): $ext=strtolower(pathinfo($f, PATHINFO_EXTENSION)); $fileUrl='in-dir/'.rawurlencode(basename($editDir)).'/'.rawurlencode($f); ?><div class="caption-row"><div class="preview"><?php if(in_array($ext,['jpg','jpeg','png','webp','gif'])): ?><img src="<?=h($fileUrl)?>" alt=""><?php elseif(in_array($ext,['mp4','mov','m4v','webm'])): ?><video src="<?=h($fileUrl)?>" muted preload="metadata"></video><?php else: ?><span><?=h(strtoupper($ext ?: 'FILE'))?></span><?php endif; ?></div><div class="caption-fields"><strong><?=h($f)?></strong><?php if(in_array($ext,['jpg','jpeg','png','webp','gif','mp4','mov','m4v','webm'])): ?><input type="hidden" name="caption_files[]" value="<?=h($f)?>"><textarea name="captions[]" placeholder="Optional caption for this file"><?=h($editData['captions'][$f] ?? '')?></textarea><?php if(in_array($ext,['mp4','mov','m4v','webm'])): ?><label class="checkbox-label"><input type="checkbox" name="use_audio_files[]" value="<?=h($f)?>" <?=!empty($editData['video_audio'][$f])?'checked':''?>> <span>Use video file audio</span></label><?php endif; ?><?php else: ?><small>No caption for audio files</small><?php endif; ?></div><button type="button" onclick="deleteFile(<?=h(json_encode($f))?>)">Remove</button></div><?php endforeach; ?></div><button type="submit">Save changes</button></form><form method="post" id="delete-file-form" style="display:none"><input type="hidden" name="action" value="delete_file"><input type="hidden" name="dir" value="<?=h(basename($editDir))?>"><input type="hidden" name="file" id="delete-file-name" value=""></form><form method="post" onsubmit="return confirm('Delete this input directory?')"><input type="hidden" name="action" value="delete_dir"><input type="hidden" name="dir" value="<?=h(basename($editDir))?>"><label class="checkbox-label"><input type="checkbox" name="delete_output" value="1"> <span>Also delete generated video, if any</span></label><button class="danger" type="submit">Delete input dir</button></form></section><?php endif; ?>
<?php if($tab==='new'): ?><section><h2>Add new movie input</h2><form method="post" enctype="multipart/form-data"><input type="hidden" name="action" value="create"><label>Title*<input name="title" required></label><label>Date<input name="date" placeholder="2026-05-25"></label><label>Place<input name="place"></label><label>Description<textarea name="description" maxlength="5000" data-counter="description-counter-new"></textarea><small id="description-counter-new" class="counter"></small></label><label>Images / videos*<input type="file" name="media[]" multiple required accept="image/*,video/*"></label><label>Audio (optional)<input type="file" name="audio[]" multiple accept="audio/*"></label><button type="submit">Create input-dir</button></form></section><?php endif; ?>
<?php if($tab==='edit' && $editDir && !$editRunning): ?><section><h2>Edit input dir</h2><p><code>in-dir/<?=h(basename($editDir))?></code></p><form method="post" enctype="multipart/form-data" id="edit-form"><input type="hidden" name="action" value="update"><input type="hidden" name="dir" value="<?=h(basename($editDir))?>"><label>Title<input name="title" value="<?=h($editData['title'])?>"></label><label>Date<input name="date" value="<?=h($editData['date'])?>"></label><label>Place<input name="place" value="<?=h($editData['place'])?>"></label><label>Description<textarea name="description" maxlength="5000" data-counter="description-counter-edit"><?=h($editData['description'])?></textarea><small id="description-counter-edit" class="counter"></small></label><label>Add images / videos<input type="file" name="media[]" multiple accept="image/*,video/*"></label><label>Add audio<input type="file" name="audio[]" multiple accept="audio/*"></label><h3>Files and captions</h3><div class="file-list"><?php foreach($editFiles as $f): $ext=strtolower(pathinfo($f, PATHINFO_EXTENSION)); $fileUrl='in-dir/'.rawurlencode(basename($editDir)).'/'.rawurlencode($f); ?><div class="caption-row"><div class="preview"><?php if(in_array($ext,['jpg','jpeg','png','webp','gif'])): ?><img src="<?=h($fileUrl)?>" alt=""><?php elseif(in_array($ext,['mp4','mov','m4v','webm'])): ?><video src="<?=h($fileUrl)?>" muted preload="metadata"></video><?php else: ?><span><?=h(strtoupper($ext ?: 'FILE'))?></span><?php endif; ?></div><div class="caption-fields"><strong><?=h($f)?></strong><?php if(in_array($ext,['jpg','jpeg','png','webp','gif','mp4','mov','m4v','webm'])): ?><input type="hidden" name="caption_files[]" value="<?=h($f)?>"><textarea name="captions[]" placeholder="Optional caption for this file"><?=h($editData['captions'][$f] ?? '')?></textarea><?php if(in_array($ext,['mp4','mov','m4v','webm'])): ?><label class="checkbox-label"><input type="checkbox" name="use_audio_files[]" value="<?=h($f)?>" <?=!empty($editData['video_audio'][$f])?'checked':''?>> <span>Use video file audio</span></label><?php endif; ?><?php else: ?><small>No caption for audio files</small><?php endif; ?></div><button type="button" onclick="deleteFile(<?=h(json_encode($f))?>)">Remove</button></div><?php endforeach; ?></div><button type="submit">Save changes</button></form><form method="post" id="delete-file-form" style="display:none"><input type="hidden" name="action" value="delete_file"><input type="hidden" name="dir" value="<?=h(basename($editDir))?>"><input type="hidden" name="file" id="delete-file-name" value=""></form><form method="post" onsubmit="return confirm(\'Delete this input directory\?\')" id="delete-dir-form""><input type="hidden" name="action" value="delete_dir"><input type="hidden" name="dir" value="<?=h(basename($editDir))?>"><label class="checkbox-label"><input type="checkbox" name="delete_output" value="1"> <span>Also delete generated video, if any</span></label><button class="danger" type="submit">Delete input dir</button></form></section><?php endif; ?>
<?php if($tab==='new'): ?><section><h2>Add new movie input</h2><form method="post" enctype="multipart/form-data" id="new-form"><input type="hidden" name="action" value="create"><label>Title*<input name="title" required></label><label>Date<input name="date" placeholder="2026-05-25"></label><label>Place<input name="place"></label><label>Description<textarea name="description" maxlength="5000" data-counter="description-counter-new"></textarea><small id="description-counter-new" class="counter"></small></label><label>Images / videos*<input type="file" name="media[]" multiple required accept="image/*,video/*"></label><label>Audio (optional)<input type="file" name="audio[]" multiple accept="audio/*"></label><button type="submit">Create input-dir</button></form></section><?php endif; ?>
<script>function deleteFile(name){if(confirm('Remove this file?')){document.getElementById('delete-file-name').value=name;document.getElementById('delete-file-form').submit();}}</script>
<script>
function showToast(message, ok){
@ -431,11 +446,75 @@ function showToast(message, ok){
setTimeout(function(){toast.remove();},250);
},3200);
}
const editForm=document.getElementById('edit-form');
if(editForm) handleAjaxFormSubmit(editForm, 'edit');
const newForm=document.getElementById('new-form');
if(newForm) handleAjaxFormSubmit(newForm, 'edit');
const deleteFileForm=document.getElementById('delete-file-form');
if(deleteFileForm) handleAjaxFormSubmit(deleteFileForm, 'edit');
const deleteDirForm=document.getElementById('delete-dir-form');
if(deleteDirForm) handleAjaxFormSubmit(deleteDirForm, 'edit');
// For old delete video forms
// Redirect tab defaults to videos
for(const form of document.querySelectorAll('.js-ajax-form')) {
handleAjaxFormSubmit(form, form.dataset.redirectTab || 'videos');
}
function handleAjaxFormSubmit(form, redirectTab) {
form.addEventListener('submit', async (event) => {
event.preventDefault();
const submit = form.querySelector('button[type="submit"]');
const data = new FormData(form);
data.set('ajax', '1');
if(submit) submit.disabled = true;
try{
const res = await fetch('new.php', { method: 'POST', body: data, credentials: 'same-origin', headers: { 'Accept': 'application/json' }});
const json = await res.json().catch(() => ({}));
if(!res.ok || !json.ok) throw new Error(json.error || 'Operation failed');
showToast(json.message || 'Operation successful', true);
setTimeout(() => { window.location.href = 'new.php?tab=' + (json.tab || redirectTab); }, 450);
} catch(e) {
showToast(e.message || 'Operation failed', false);
} finally {
if(submit) submit.disabled = false;
}
});
}
const initialMessage = <?= json_encode($msg ?? '') ?>;
if (initialMessage) showToast(initialMessage, true);
const editForm=document.getElementById('edit-form');
if(editForm){
editForm.addEventListener('submit', async function(event){
event.preventDefault();
const submit=editForm.querySelector('button[type="submit"]');
const data=new FormData(editForm);
data.set('ajax','1');
if(submit) submit.disabled=true;
try{
const res=await fetch('new.php',{method:'POST',body:data,credentials:'same-origin',headers:{'Accept':'application/json'}});
const json=await res.json().catch(function(){return {};});
if(!res.ok || !json.ok) throw new Error(json.error || 'Save failed');
showToast(json.message || ('Updated input directory: in-dir/' + (json.dir || '')), true);
setTimeout(function(){ window.location.href='new.php?tab=edit'; }, 450);
}catch(e){
showToast(e.message || 'Save failed', false);
}finally{
if(submit) submit.disabled=false;
}
});
}
document.querySelectorAll('.admin-switches input[type="checkbox"]').forEach(function(input){
input.addEventListener('change', async function(event){
event.preventDefault();
const form=input.form;
const previous=!input.checked;
const row=input.closest('.admin-video-row');
const data=new FormData(form);
data.set('ajax','1');
input.disabled=true;
@ -443,6 +522,9 @@ document.querySelectorAll('.admin-switches input[type="checkbox"]').forEach(func
const res=await fetch('new.php',{method:'POST',body:data,credentials:'same-origin',headers:{'Accept':'application/json'}});
const json=await res.json().catch(function(){return {};});
if(!res.ok || !json.ok) throw new Error(json.error || 'Switch update failed');
if(data.get('action')==='set_visible' && row){
row.classList.toggle('shown', input.checked);
}
const action=data.get('action');
const titleEl=input.closest('.video-info')?.querySelector('h2');
const title=(titleEl?.textContent || '').trim() || json.dir || data.get('dir') || 'input dir';
@ -452,6 +534,9 @@ document.querySelectorAll('.admin-switches input[type="checkbox"]').forEach(func
else showToast('Updated '+quotedTitle, true);
}catch(e){
input.checked=previous;
if(data.get('action')==='set_visible' && row){
row.classList.toggle('shown', input.checked);
}
showToast(e.message || 'Switch update failed', false);
}finally{
input.disabled=false;
@ -465,7 +550,18 @@ document.querySelectorAll('.admin-switches input[type="checkbox"]').forEach(func
if(!box) return;
function fmt(job){
const name=(job.name||'job').replace(/_/g,' ');
return '<span class="job-dot"></span><span>'+name+'</span>';
const started=job.started_at ? new Date(job.started_at) : null;
const ageText = started && !Number.isNaN(started.getTime()) ? ' <span class="job-age">(' + formatAge(started) + ')</span>' : '';
return '<span class="job-dot"></span><span>'+name+'</span>'+ageText;
}
function formatAge(started){
const seconds = Math.max(0, Math.floor((Date.now() - started.getTime()) / 1000));
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = seconds % 60;
if (h > 0) return h + 'h ' + m + 'm';
if (m > 0) return m + 'm ' + s + 's';
return s + 's';
}
function syncEditButtons(runningJobs){
document.querySelectorAll('.admin-video-row[data-job]').forEach(function(row){
@ -498,15 +594,21 @@ document.querySelectorAll('.admin-switches input[type="checkbox"]').forEach(func
const r=await fetch('job_status.php',{cache:'no-store'});
if(!r.ok) throw new Error('status failed');
const data=await r.json();
const jobs=data.jobs||[];
const runningJobs=new Set(jobs.map(function(job){ return job && job.name ? String(job.name) : ''; }).filter(Boolean));
const jobs=(data.jobs||[]).filter(function(job){ return job && job.name; });
const runningJobs=new Set(jobs.map(function(job){ return String(job.name); }));
syncEditButtons(runningJobs);
if(!jobs.length){box.hidden=true;box.innerHTML='';return;}
if(!jobs.length){
box.hidden=true;
box.innerHTML='';
return;
}
box.hidden=false;
box.innerHTML='<strong>Running</strong>'+jobs.map(fmt).join('');
}catch(e){box.hidden=true;}
box.innerHTML=jobs.map(fmt).join('');
}catch(e){
box.hidden=true;
}
}
updateJobs();
setInterval(updateJobs,30000);
})();
</script></main><footer>Input dirs are saved under <code>in-dir/</code>.</footer><script>document.querySelectorAll('textarea[data-counter]').forEach(function(t){var c=document.getElementById(t.dataset.counter);function u(){c.textContent=t.value.length+' / '+t.maxLength+' characters';}t.addEventListener('input',u);u();});</script></body></html>
</script></main><footer>Input dirs are saved under <code>in-dir/</code>.</footer><script>document.querySelectorAll('textarea[data-counter]').forEach(function(t){var c=document.getElementById(t.dataset.counter);function u(){c.textContent=t.value.length+' / '+t.maxLength+' characters';}t.addEventListener('input',u);u();});</script><footer class="site-footer admin-footer"><div class="admin-left"><nav class="tabs"><a class="<?= $tab==='new'?'active':'' ?>" href="new.php?tab=new">New</a><a class="<?= $tab==='edit'?'active':'' ?>" href="new.php?tab=edit">Edit</a><a class="<?= $tab==='videos'?'active':'' ?>" href="new.php?tab=videos">Videos</a></nav></div><div id="job-status" class="job-status" hidden></div><div class="admin-right"><a class="logout-link" href="logout.php">Log off</a></div></footer></body></html>

File diff suppressed because one or more lines are too long