From be890808c36e3c237b577066aaacfd63b1ba8ad2 Mon Sep 17 00:00:00 2001 From: hbrain Date: Mon, 25 May 2026 19:08:44 +0200 Subject: [PATCH] Add admin input directory editor --- new.php | 84 ++++++++++++++++++++++++++++++++++++++++++------------- style.css | 2 +- 2 files changed, 66 insertions(+), 20 deletions(-) diff --git a/new.php b/new.php index c45b1da..8c8d1a8 100644 --- a/new.php +++ b/new.php @@ -4,6 +4,27 @@ foreach (["videos_dir", "thumbs_dir", "uploads_dir"] as $d) if (!is_dir($config[ function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, "UTF-8"); } function slugify($s){ $s = strtolower(trim($s)); $s = preg_replace('/[^a-z0-9]+/', '-', $s); return trim($s, '-') ?: 'movie'; } function rrmdir($dir){ if(!is_dir($dir)) return; foreach(scandir($dir) as $f){ if($f==='.'||$f==='..') continue; $p="$dir/$f"; is_dir($p)?rrmdir($p):unlink($p);} rmdir($dir); } +function input_dirs($base){ $dirs = glob($base.'/*', GLOB_ONLYDIR) ?: []; usort($dirs, fn($a,$b)=>filemtime($b)<=>filemtime($a)); return $dirs; } +function safe_input_dir($base, $name){ $name = basename((string)$name); $path = realpath($base . '/' . $name); $root = realpath($base); if (!$path || !$root || !str_starts_with($path, $root . DIRECTORY_SEPARATOR) || !is_dir($path)) throw new RuntimeException('Invalid input directory.'); return $path; } +function read_data($dir){ + $data = ['title'=>'','date'=>'','place'=>'','description'=>'']; $file = $dir . '/data.txt'; + if (!is_file($file)) return $data; + foreach (file($file, FILE_IGNORE_NEW_LINES) as $line) { + if (!str_contains($line, ':')) continue; + [$k,$v] = array_map('trim', explode(':', $line, 2)); + $k = strtolower($k); if ($k === 'location') $k = 'place'; + if (array_key_exists($k, $data)) $data[$k] = $v; + } + return $data; +} +function write_data($dir, $post){ + $lines = []; + foreach (['title'=>'Title','date'=>'Date','place'=>'Place','description'=>'Description'] as $k=>$label) { + $v = trim($post[$k] ?? ''); if ($v !== '') $lines[] = "$label: $v"; + } + file_put_contents($dir . '/data.txt', implode("\n", $lines) . "\n"); +} +function list_files($dir){ $files = array_values(array_filter(scandir($dir), fn($f)=>$f!=='.' && $f!=='..' && is_file($dir.'/'.$f) && $f !== 'data.txt')); natcasesort($files); return $files; } function save_uploads($field, $dest, $allowed){ if (empty($_FILES[$field])) return []; $files = $_FILES[$field]; $saved = []; @@ -14,34 +35,59 @@ function save_uploads($field, $dest, $allowed){ $ext = strtolower(pathinfo($files['name'][$i], PATHINFO_EXTENSION)); if (!in_array($ext, $allowed, true)) throw new RuntimeException("File type not allowed: ".$files['name'][$i]); $base = preg_replace('/[^A-Za-z0-9._-]+/', '_', basename($files['name'][$i])); - $target = $dest . '/' . sprintf('%03d_', count($saved)+1) . $base; + $target = $dest . '/' . sprintf('%03d_', count(glob($dest.'/*') ?: [])+count($saved)+1) . $base; if (!move_uploaded_file($files['tmp_name'][$i], $target)) throw new RuntimeException("Cannot save upload"); $saved[] = basename($target); } return $saved; } $msg = $err = null; -if ($_SERVER['REQUEST_METHOD'] === 'POST') { - try { - $title = trim($_POST['title'] ?? ''); - if ($title === '') throw new RuntimeException('Title is required.'); - $slug = date('Ymd_His') . '_' . slugify($title); - $dir = $config['uploads_dir'] . '/' . $slug; - if (!mkdir($dir, 0775, true)) throw new RuntimeException('Cannot create input directory.'); - $media = save_uploads('media', $dir, ['jpg','jpeg','png','webp','gif','mp4','mov','m4v','avi','mkv','webm']); - if (!$media) { rrmdir($dir); throw new RuntimeException('Upload at least one image or video.'); } - save_uploads('audio', $dir, ['mp3','wav','m4a','aac','ogg','flac']); - $lines = []; - foreach (['title'=>'Title','date'=>'Date','place'=>'Place','description'=>'Description'] as $k=>$label) { - $v = trim($_POST[$k] ?? ''); if ($v !== '') $lines[] = "$label: $v"; +$allowedMedia = ['jpg','jpeg','png','webp','gif','mp4','mov','m4v','avi','mkv','webm']; +$allowedAudio = ['mp3','wav','m4a','aac','ogg','flac']; +try { + if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $action = $_POST['action'] ?? 'create'; + if ($action === 'create') { + $title = trim($_POST['title'] ?? ''); + if ($title === '') throw new RuntimeException('Title is required.'); + $slug = date('Ymd_His') . '_' . slugify($title); + $dir = $config['uploads_dir'] . '/' . $slug; + if (!mkdir($dir, 0775, true)) throw new RuntimeException('Cannot create input directory.'); + $media = save_uploads('media', $dir, $allowedMedia); + if (!$media) { rrmdir($dir); throw new RuntimeException('Upload at least one image or video.'); } + save_uploads('audio', $dir, $allowedAudio); + write_data($dir, $_POST); + $msg = "Created movmaker input directory: in-dir/$slug"; + $_GET['edit'] = $slug; + } elseif ($action === 'update') { + $dir = safe_input_dir($config['uploads_dir'], $_POST['dir'] ?? ''); + write_data($dir, $_POST); + save_uploads('media', $dir, $allowedMedia); + save_uploads('audio', $dir, $allowedAudio); + $msg = 'Updated input directory: in-dir/' . basename($dir); + $_GET['edit'] = basename($dir); + } elseif ($action === 'delete_file') { + $dir = safe_input_dir($config['uploads_dir'], $_POST['dir'] ?? ''); + $file = basename((string)($_POST['file'] ?? '')); + if ($file === '' || $file === 'data.txt' || !is_file($dir.'/'.$file)) throw new RuntimeException('Invalid file.'); + unlink($dir.'/'.$file); + $msg = 'Removed file: ' . $file; + $_GET['edit'] = basename($dir); + } elseif ($action === 'delete_dir') { + $dir = safe_input_dir($config['uploads_dir'], $_POST['dir'] ?? ''); + $name = basename($dir); rrmdir($dir); $msg = 'Deleted input directory: in-dir/' . $name; } - file_put_contents($dir . '/data.txt', implode("\n", $lines) . "\n"); - $msg = "Created movmaker input directory: in-dir/$slug"; - } catch (Throwable $e) { $err = $e->getMessage(); } -} + } +} catch (Throwable $e) { $err = $e->getMessage(); } +$dirs = input_dirs($config['uploads_dir']); +$editName = $_GET['edit'] ?? ''; +$editDir = null; $editData = ['title'=>'','date'=>'','place'=>'','description'=>'']; $editFiles = []; +if ($editName !== '') { try { $editDir = safe_input_dir($config['uploads_dir'], $editName); $editData = read_data($editDir); $editFiles = list_files($editDir); } catch (Throwable $e) { $err = $e->getMessage(); } } ?> Admin - <?=h($config['site_name'])?>

MVLog Admin

-

Add new movie input

+

Existing input dirs

No input directories yet.


ยท files
Edit
+

Edit input dir

in-dir/

Files

+

Add new movie input

diff --git a/style.css b/style.css index b049e4f..24c6458 100644 --- a/style.css +++ b/style.css @@ -1 +1 @@ -@font-face{font-family:BebasNeue;src:url('assets/fonts/BebasNeue-Regular.ttf')}@font-face{font-family:IBMPlexCaption;src:url('assets/fonts/IBMPlexSans-SemiBold.ttf')}@font-face{font-family:NotoStamp;src:url('assets/fonts/NotoSans-Bold.ttf')}:root{font-family:IBMPlexCaption,system-ui,sans-serif;color:#F3F4F6;background:#111315}body{margin:0;background:#111315 url(assets/img/background.jpg) center center/cover fixed no-repeat;min-height:100vh}body::before{content:'';position:fixed;inset:0;background:rgba(17,19,21,.78);z-index:-1}header{background:#1B1E22;color:#F3F4F6;padding:1.2rem 2rem;text-align:center}header h1{font-family:BebasNeue,system-ui,sans-serif;font-size:3rem;letter-spacing:.06em;margin:0}a{color:#3BA7A0}.button,button{background:#C46A3A;color:#111315;border:0;border-radius:.5rem;padding:.7rem 1rem;text-decoration:none;cursor:pointer}nav a{margin-left:1rem;color:#F3F4F6}main{max-width:1100px;margin:2rem auto;padding:0 1rem}.video-list{display:flex;flex-direction:column;gap:1rem}.video-row,form{background:rgba(27,30,34,.92);backdrop-filter:blur(2px);border-radius:.8rem;padding:1rem;box-shadow:0 2px 16px #0006}.video-row{display:grid;grid-template-columns:minmax(280px,42%) 1fr;gap:1rem;align-items:start}.video-row video{width:100%;aspect-ratio:16/9;background:#111315;border-radius:.5rem}.video-info h2{font-size:1.6rem;margin:.1rem 0 .5rem}.meta{display:flex;gap:.5rem;flex-wrap:wrap;color:#A0A4AB;margin:.25rem 0}.meta span{background:#26383A;border-radius:999px;padding:.25rem .55rem}.description{font-size:1rem;line-height:1.45;color:#F3F4F6;margin:.9rem 0}.details{color:#A0A4AB;font-size:.9rem}label{display:block;margin:.8rem 0;font-weight:600}input,textarea{display:block;width:100%;box-sizing:border-box;margin-top:.3rem;padding:.65rem;border:1px solid #3BA7A0;background:#111315;color:#F3F4F6;border-radius:.45rem}textarea{min-height:6rem}.ok,.err{padding:1rem;border-radius:.5rem;margin-bottom:1rem}.ok{background:#1f3b32;color:#F3F4F6}.err{background:#4a241f;color:#F3F4F6}.pages a{display:inline-block;margin:.2rem;padding:.45rem .7rem;background:#1B1E22;border-radius:.4rem;text-decoration:none}.pages .active{background:#C46A3A;color:#111315}footer{font-family:NotoStamp,system-ui,sans-serif;text-align:center;color:#A0A4AB;padding:2rem;font-size:.8rem}@media(max-width:760px){.video-row{grid-template-columns:1fr}header h1{font-size:2.4rem}} +@font-face{font-family:BebasNeue;src:url('assets/fonts/BebasNeue-Regular.ttf')}@font-face{font-family:IBMPlexCaption;src:url('assets/fonts/IBMPlexSans-SemiBold.ttf')}@font-face{font-family:NotoStamp;src:url('assets/fonts/NotoSans-Bold.ttf')}:root{font-family:IBMPlexCaption,system-ui,sans-serif;color:#F3F4F6;background:#111315}body{margin:0;background:#111315 url(assets/img/background.jpg) center center/cover fixed no-repeat;min-height:100vh}body::before{content:'';position:fixed;inset:0;background:rgba(17,19,21,.78);z-index:-1}header{background:#1B1E22;color:#F3F4F6;padding:1.2rem 2rem;text-align:center}header h1{font-family:BebasNeue,system-ui,sans-serif;font-size:3rem;letter-spacing:.06em;margin:0}a{color:#3BA7A0}.button,button{background:#C46A3A;color:#111315;border:0;border-radius:.5rem;padding:.7rem 1rem;text-decoration:none;cursor:pointer}nav a{margin-left:1rem;color:#F3F4F6}main{max-width:1100px;margin:2rem auto;padding:0 1rem}.video-list{display:flex;flex-direction:column;gap:1rem}.video-row,form{background:rgba(27,30,34,.92);backdrop-filter:blur(2px);border-radius:.8rem;padding:1rem;box-shadow:0 2px 16px #0006}.video-row{display:grid;grid-template-columns:minmax(280px,42%) 1fr;gap:1rem;align-items:start}.video-row video{width:100%;aspect-ratio:16/9;background:#111315;border-radius:.5rem}.video-info h2{font-size:1.6rem;margin:.1rem 0 .5rem}.meta{display:flex;gap:.5rem;flex-wrap:wrap;color:#A0A4AB;margin:.25rem 0}.meta span{background:#26383A;border-radius:999px;padding:.25rem .55rem}.description{font-size:1rem;line-height:1.45;color:#F3F4F6;margin:.9rem 0}.details{color:#A0A4AB;font-size:.9rem}label{display:block;margin:.8rem 0;font-weight:600}input,textarea{display:block;width:100%;box-sizing:border-box;margin-top:.3rem;padding:.65rem;border:1px solid #3BA7A0;background:#111315;color:#F3F4F6;border-radius:.45rem}textarea{min-height:6rem}.ok,.err{padding:1rem;border-radius:.5rem;margin-bottom:1rem}.ok{background:#1f3b32;color:#F3F4F6}.err{background:#4a241f;color:#F3F4F6}.pages a{display:inline-block;margin:.2rem;padding:.45rem .7rem;background:#1B1E22;border-radius:.4rem;text-decoration:none}.pages .active{background:#C46A3A;color:#111315}footer{font-family:NotoStamp,system-ui,sans-serif;text-align:center;color:#A0A4AB;padding:2rem;font-size:.8rem}.admin-list{display:flex;flex-direction:column;gap:.7rem}.admin-item,.file-row{display:flex;justify-content:space-between;align-items:center;gap:1rem;background:rgba(27,30,34,.92);padding:.8rem;border-radius:.6rem}.admin-item span,.file-row span{color:#A0A4AB}.file-list{display:flex;flex-direction:column;gap:.5rem;margin:1rem 0}.file-row{margin:0}.file-row button{padding:.45rem .7rem}@media(max-width:760px){.video-row{grid-template-columns:1fr}header h1{font-size:2.4rem}}