Initial movmaker web UI
This commit is contained in:
commit
7670a52350
4 changed files with 127 additions and 0 deletions
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
AGENTS.md
|
||||||
|
uploads/
|
||||||
|
videos/
|
||||||
|
thumbs/
|
||||||
|
*.log
|
||||||
10
config.php
Normal file
10
config.php
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?php
|
||||||
|
return [
|
||||||
|
"site_name" => "Movmaker WebUI",
|
||||||
|
"videos_dir" => __DIR__ . "/videos",
|
||||||
|
"thumbs_dir" => __DIR__ . "/thumbs",
|
||||||
|
"uploads_dir" => __DIR__ . "/uploads",
|
||||||
|
"public_videos" => "videos",
|
||||||
|
"public_thumbs" => "thumbs",
|
||||||
|
"items_per_page" => 12,
|
||||||
|
];
|
||||||
111
index.php
Normal file
111
index.php
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
<?php
|
||||||
|
$config = require __DIR__ . "/config.php";
|
||||||
|
foreach (["videos_dir", "thumbs_dir", "uploads_dir"] as $d) if (!is_dir($config[$d])) mkdir($config[$d], 0775, true);
|
||||||
|
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 save_uploads($field, $dest, $allowed){
|
||||||
|
if (empty($_FILES[$field])) return [];
|
||||||
|
$files = $_FILES[$field]; $saved = [];
|
||||||
|
$count = is_array($files[name]) ? count($files[name]) : 0;
|
||||||
|
for ($i=0; $i<$count; $i++) {
|
||||||
|
if ($files[error][$i] === UPLOAD_ERR_NO_FILE) continue;
|
||||||
|
if ($files[error][$i] !== UPLOAD_ERR_OK) throw new RuntimeException("Upload failed: ".$files[name][$i]);
|
||||||
|
$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;
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
file_put_contents($dir . /data.txt, implode("\n", $lines) . "\n");
|
||||||
|
$msg = "Created movmaker input directory: uploads/$slug";
|
||||||
|
} catch (Throwable $e) { $err = $e->getMessage(); }
|
||||||
|
}
|
||||||
|
$videos = glob($config[videos_dir]./*.mp4, GLOB_BRACE) ?: [];
|
||||||
|
usort($videos, fn($a,$b)=>filemtime($b)<=>filemtime($a));
|
||||||
|
$page=max(1,(int)($_GET[page]??1)); $per=$config[items_per_page]; $total=count($videos); $pages=max(1,(int)ceil($total/$per)); $page=min($page,$pages); $slice=array_slice($videos,($page-1)*$per,$per);
|
||||||
|
?>
|
||||||
|
<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title><?=h($config[site_name])?></title><link rel="stylesheet" href="style.css"></head><body>
|
||||||
|
<header><h1><?=h($config[site_name])?></h1><nav><a href="#videos">Videos</a><a class="button" href="#new">Add new</a></nav></header><main>
|
||||||
|
<?php if($msg): ?><div class="ok"><?=h($msg)?></div><?php endif; ?><?php if($err): ?><div class="err"><?=h($err)?></div><?php endif; ?>
|
||||||
|
<section id="videos"><h2>Videos</h2><?php if(!$slice): ?><p>No videos yet. Put generated movies in <code>videos/</code>.</p><?php endif; ?><div class="grid">
|
||||||
|
<?php foreach($slice as $v): $name=basename($v); $url=$config[public_videos]./.rawurlencode($name); $size=filesize($v); ?>
|
||||||
|
<article class="card"><video controls preload="metadata" src="<?=h($url)?>"></video><h3><?=h(pathinfo($name,PATHINFO_FILENAME))?></h3><p><?=h(date(Y-m-d 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";
|
||||||
|
}
|
||||||
|
file_put_contents($dir . /data.txt, implode("\n", $lines) . "\n");
|
||||||
|
$msg = "Created movmaker input directory: uploads/$slug";
|
||||||
|
} catch (Throwable $e) { $err = $e->getMessage(); }
|
||||||
|
}
|
||||||
|
$videos = glob($config[videos_dir]./*.webm, GLOB_BRACE) ?: [];
|
||||||
|
usort($videos, fn($a,$b)=>filemtime($b)<=>filemtime($a));
|
||||||
|
$page=max(1,(int)($_GET[page]??1)); $per=$config[items_per_page]; $total=count($videos); $pages=max(1,(int)ceil($total/$per)); $page=min($page,$pages); $slice=array_slice($videos,($page-1)*$per,$per);
|
||||||
|
?>
|
||||||
|
<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title><?=h($config[site_name])?></title><link rel="stylesheet" href="style.css"></head><body>
|
||||||
|
<header><h1><?=h($config[site_name])?></h1><nav><a href="#videos">Videos</a><a class="button" href="#new">Add new</a></nav></header><main>
|
||||||
|
<?php if($msg): ?><div class="ok"><?=h($msg)?></div><?php endif; ?><?php if($err): ?><div class="err"><?=h($err)?></div><?php endif; ?>
|
||||||
|
<section id="videos"><h2>Videos</h2><?php if(!$slice): ?><p>No videos yet. Put generated movies in <code>videos/</code>.</p><?php endif; ?><div class="grid">
|
||||||
|
<?php foreach($slice as $v): $name=basename($v); $url=$config[public_videos]./.rawurlencode($name); $size=filesize($v); ?>
|
||||||
|
<article class="card"><video controls preload="metadata" src="<?=h($url)?>"></video><h3><?=h(pathinfo($name,PATHINFO_FILENAME))?></h3><p><?=h(date(Y-m-d 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";
|
||||||
|
}
|
||||||
|
file_put_contents($dir . /data.txt, implode("\n", $lines) . "\n");
|
||||||
|
$msg = "Created movmaker input directory: uploads/$slug";
|
||||||
|
} catch (Throwable $e) { $err = $e->getMessage(); }
|
||||||
|
}
|
||||||
|
$videos = glob($config[videos_dir]./*.mov, GLOB_BRACE) ?: [];
|
||||||
|
usort($videos, fn($a,$b)=>filemtime($b)<=>filemtime($a));
|
||||||
|
$page=max(1,(int)($_GET[page]??1)); $per=$config[items_per_page]; $total=count($videos); $pages=max(1,(int)ceil($total/$per)); $page=min($page,$pages); $slice=array_slice($videos,($page-1)*$per,$per);
|
||||||
|
?>
|
||||||
|
<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title><?=h($config[site_name])?></title><link rel="stylesheet" href="style.css"></head><body>
|
||||||
|
<header><h1><?=h($config[site_name])?></h1><nav><a href="#videos">Videos</a><a class="button" href="#new">Add new</a></nav></header><main>
|
||||||
|
<?php if($msg): ?><div class="ok"><?=h($msg)?></div><?php endif; ?><?php if($err): ?><div class="err"><?=h($err)?></div><?php endif; ?>
|
||||||
|
<section id="videos"><h2>Videos</h2><?php if(!$slice): ?><p>No videos yet. Put generated movies in <code>videos/</code>.</p><?php endif; ?><div class="grid">
|
||||||
|
<?php foreach($slice as $v): $name=basename($v); $url=$config[public_videos]./.rawurlencode($name); $size=filesize($v); ?>
|
||||||
|
<article class="card"><video controls preload="metadata" src="<?=h($url)?>"></video><h3><?=h(pathinfo($name,PATHINFO_FILENAME))?></h3><p><?=h(date(Y-m-d 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";
|
||||||
|
}
|
||||||
|
file_put_contents($dir . /data.txt, implode("\n", $lines) . "\n");
|
||||||
|
$msg = "Created movmaker input directory: uploads/$slug";
|
||||||
|
} catch (Throwable $e) { $err = $e->getMessage(); }
|
||||||
|
}
|
||||||
|
$videos = glob($config[videos_dir]./*.m4v, GLOB_BRACE) ?: [];
|
||||||
|
usort($videos, fn($a,$b)=>filemtime($b)<=>filemtime($a));
|
||||||
|
$page=max(1,(int)($_GET[page]??1)); $per=$config[items_per_page]; $total=count($videos); $pages=max(1,(int)ceil($total/$per)); $page=min($page,$pages); $slice=array_slice($videos,($page-1)*$per,$per);
|
||||||
|
?>
|
||||||
|
<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title><?=h($config[site_name])?></title><link rel="stylesheet" href="style.css"></head><body>
|
||||||
|
<header><h1><?=h($config[site_name])?></h1><nav><a href="#videos">Videos</a><a class="button" href="#new">Add new</a></nav></header><main>
|
||||||
|
<?php if($msg): ?><div class="ok"><?=h($msg)?></div><?php endif; ?><?php if($err): ?><div class="err"><?=h($err)?></div><?php endif; ?>
|
||||||
|
<section id="videos"><h2>Videos</h2><?php if(!$slice): ?><p>No videos yet. Put generated movies in <code>videos/</code>.</p><?php endif; ?><div class="grid">
|
||||||
|
<?php foreach($slice as $v): $name=basename($v); $url=$config[public_videos]./.rawurlencode($name); $size=filesize($v); ?>
|
||||||
|
<article class="card"><video controls preload="metadata" src="<?=h($url)?>"></video><h3><?=h(pathinfo($name,PATHINFO_FILENAME))?></h3><p><?=h(date(Y-m-d H:i,filemtime($v)))?> · <?=h(round($size/1048576,1))?> MB</p><a href="<?=h($url)?>" download>Download</a></article>
|
||||||
|
<?php endforeach; ?></div><div class="pages"><?php for($i=1;$i<=$pages;$i++): ?><a class="<?=$i===$page?active:?>" href="?page=<?=$i?>#videos"><?=$i?></a><?php endfor; ?></div></section>
|
||||||
|
<section id="new"><h2>Add new movie input</h2><form method="post" enctype="multipart/form-data"><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"></textarea></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>
|
||||||
|
</main><footer>Input dirs are saved under <code>uploads/</code>; generated videos are listed from <code>videos/</code>.</footer></body></html>
|
||||||
1
style.css
Normal file
1
style.css
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
:root{font-family:system-ui,sans-serif;color:#17202a;background:#f6f7fb}body{margin:0}header{background:#101827;color:white;padding:1.2rem 2rem;display:flex;justify-content:space-between;align-items:center}a{color:#2563eb}.button,button{background:#2563eb;color:white;border:0;border-radius:.5rem;padding:.7rem 1rem;text-decoration:none;cursor:pointer}nav a{margin-left:1rem;color:white}main{max-width:1100px;margin:2rem auto;padding:0 1rem}.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:1rem}.card,form{background:white;border-radius:.8rem;padding:1rem;box-shadow:0 2px 10px #0001}.card video{width:100%;aspect-ratio:16/9;background:#111;border-radius:.5rem}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 #cbd5e1;border-radius:.45rem}textarea{min-height:6rem}.ok,.err{padding:1rem;border-radius:.5rem;margin-bottom:1rem}.ok{background:#dcfce7}.err{background:#fee2e2}.pages a{display:inline-block;margin:.2rem;padding:.45rem .7rem;background:white;border-radius:.4rem;text-decoration:none}.pages .active{background:#2563eb;color:white}footer{text-align:center;color:#667085;padding:2rem}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue