- unconditional map regeneration on Save changes
- logging to /var/log/mvlog_map.log
This commit is contained in:
parent
2745e1cc35
commit
129711d798
3 changed files with 373 additions and 56 deletions
88
index.php
88
index.php
|
|
@ -57,16 +57,60 @@ function map_hidden_outputs($config){
|
||||||
return $hidden;
|
return $hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function public_url_path($path){
|
||||||
|
$path = str_replace('\\', '/', trim((string)$path));
|
||||||
|
if ($path === '') return '';
|
||||||
|
$parts = array_values(array_filter(explode('/', $path), static fn($p) => $p !== ''));
|
||||||
|
return implode('/', array_map('rawurlencode', $parts));
|
||||||
|
}
|
||||||
function video_thumb_public_url($config, $videoName){
|
function video_thumb_public_url($config, $videoName){
|
||||||
$videoName = basename((string)$videoName);
|
$videoName = basename((string)$videoName);
|
||||||
if ($videoName === '') return '';
|
if ($videoName === '') return '';
|
||||||
$base = pathinfo($videoName, PATHINFO_FILENAME);
|
$info = function_exists('video_thumb_source_info') ? video_thumb_source_info($config, $videoName) : [];
|
||||||
foreach (['jpg','jpeg','png','webp','gif'] as $ext) {
|
$sourcePath = (string)($info['source_path'] ?? '');
|
||||||
$file = $config['videos_dir'] . '/' . $base . '_thumb.' . $ext;
|
if ($sourcePath !== '') {
|
||||||
if (is_file($file)) return $config['public_videos'] . '/' . rawurlencode($base . '_thumb.' . $ext) . '?v=' . filemtime($file);
|
$abs = str_starts_with($sourcePath, '/') ? $sourcePath : __DIR__ . '/' . ltrim($sourcePath, '/');
|
||||||
|
if (is_file($abs)) return public_url_path($sourcePath) . '?v=' . filemtime($abs);
|
||||||
|
}
|
||||||
|
$sourceFile = basename((string)($info['source_file'] ?? ''));
|
||||||
|
$inputDir = basename((string)($info['input_dir'] ?? ''));
|
||||||
|
if ($sourceFile !== '' && $inputDir !== '') {
|
||||||
|
$rel = 'in-dir/' . $inputDir . '/' . $sourceFile;
|
||||||
|
$abs = __DIR__ . '/' . $rel;
|
||||||
|
if (is_file($abs)) return public_url_path($rel) . '?v=' . filemtime($abs);
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
function video_thumb_source_info($config, $videoName){
|
||||||
|
$videoName = basename((string)$videoName);
|
||||||
|
if ($videoName === '') return [];
|
||||||
|
$base = pathinfo($videoName, PATHINFO_FILENAME);
|
||||||
|
$file = $config['videos_dir'] . '/' . $base . '_thumb.json';
|
||||||
|
if (!is_file($file)) return [];
|
||||||
|
$data = json_decode((string)file_get_contents($file), true);
|
||||||
|
return is_array($data) ? $data : [];
|
||||||
|
}
|
||||||
|
function video_article_id($config, $videoName){
|
||||||
|
$videoName = basename((string)$videoName);
|
||||||
|
if ($videoName === '') return '';
|
||||||
|
foreach (glob($config['uploads_dir'].'/*', GLOB_ONLYDIR) ?: [] as $dir) {
|
||||||
|
$stateFile = $dir . '/.movmaker-state.json';
|
||||||
|
if (!is_file($stateFile)) continue;
|
||||||
|
$state = json_decode((string)file_get_contents($stateFile), true);
|
||||||
|
if (!is_array($state)) continue;
|
||||||
|
$output = basename((string)($state['output'] ?? ''));
|
||||||
|
$previewOutput = basename((string)($state['preview_output'] ?? ''));
|
||||||
|
if ($videoName === $output || $videoName === $previewOutput) return (string)($state['article_id'] ?? '');
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
function article_og_image_url($articleId){
|
||||||
|
$articleId = strtolower(trim((string)$articleId));
|
||||||
|
if (!preg_match('/^[0-9]{14}[a-f0-9]{16}$/', $articleId)) return '';
|
||||||
|
$path = __DIR__ . '/out-dir/' . $articleId . '_og.jpg';
|
||||||
|
if (!is_file($path)) return '';
|
||||||
|
return 'https://bubulescu.org/out-dir/' . rawurlencode($articleId . '_og.jpg');
|
||||||
|
}
|
||||||
|
|
||||||
function video_metadata($path){
|
function video_metadata($path){
|
||||||
$base = pathinfo($path, PATHINFO_FILENAME);
|
$base = pathinfo($path, PATHINFO_FILENAME);
|
||||||
|
|
@ -181,8 +225,8 @@ usort($videos, function($a, $b) use ($videoCache) {
|
||||||
$articleId = trim((string)($_GET['id'] ?? ''));
|
$articleId = trim((string)($_GET['id'] ?? ''));
|
||||||
|
|
||||||
if ($articleId !== '') {
|
if ($articleId !== '') {
|
||||||
$videos = array_values(array_filter($videos, function($v) use ($articleId) {
|
$videos = array_values(array_filter($videos, function($v) use ($articleId, $config) {
|
||||||
return pathinfo($v, PATHINFO_FILENAME) === $articleId;
|
return video_article_id($config, basename($v)) === $articleId;
|
||||||
}));
|
}));
|
||||||
$rawKeyword = '';
|
$rawKeyword = '';
|
||||||
$keywordType = 'general';
|
$keywordType = 'general';
|
||||||
|
|
@ -248,6 +292,21 @@ $slice = array_slice($videos, ($page - 1) * $per, $per);
|
||||||
|
|
||||||
$activeFilterCount = ($keywordValue !== '') ? 1 : 0;
|
$activeFilterCount = ($keywordValue !== '') ? 1 : 0;
|
||||||
$baseParams = ($rawKeyword !== '') ? ['q' => $rawKeyword] : [];
|
$baseParams = ($rawKeyword !== '') ? ['q' => $rawKeyword] : [];
|
||||||
|
$defaultOgImageUrl = 'https://bubulescu.org/assets/img/bubulescuorg.jpg';
|
||||||
|
$ogImageUrl = $defaultOgImageUrl;
|
||||||
|
$ogImageAlt = 'MVLog: journal of an unreliable narrator.';
|
||||||
|
if ($articleId !== '') {
|
||||||
|
$customOg = article_og_image_url($articleId);
|
||||||
|
if ($customOg !== '') {
|
||||||
|
$ogImageUrl = $customOg;
|
||||||
|
if (!empty($slice)) {
|
||||||
|
$firstVideo = basename((string)$slice[0]);
|
||||||
|
$firstMeta = $videoCache[$firstVideo]['metadata'] ?? video_metadata($slice[0]);
|
||||||
|
$firstTitle = trim((string)($firstMeta['title'] ?? ''));
|
||||||
|
if ($firstTitle !== '') $ogImageAlt = 'MVLog: ' . $firstTitle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
$headerTitles = [
|
$headerTitles = [
|
||||||
'Multiple Voices Log',
|
'Multiple Voices Log',
|
||||||
'MultiVerse Log',
|
'MultiVerse Log',
|
||||||
|
|
@ -307,15 +366,15 @@ $siteHeaderTitle = $headerTitles[array_rand($headerTitles)];
|
||||||
<meta property="og:site_name" content="Bubulescu.Org">
|
<meta property="og:site_name" content="Bubulescu.Org">
|
||||||
|
|
||||||
<!-- Image -->
|
<!-- Image -->
|
||||||
<meta property="og:image" content="https://bubulescu.org/assets/img/bubulescuorg.jpg">
|
<meta property="og:image" content="<?=h($ogImageUrl)?>">
|
||||||
<meta property="og:image:secure_url" content="https://bubulescu.org/assets/img/bubulescuorg.jpg">
|
<meta property="og:image:secure_url" content="<?=h($ogImageUrl)?>">
|
||||||
<meta property="og:image:type" content="image/jpeg">
|
<meta property="og:image:type" content="image/jpeg">
|
||||||
<meta property="og:image:width" content="1200">
|
<meta property="og:image:width" content="1200">
|
||||||
<meta property="og:image:height" content="630">
|
<meta property="og:image:height" content="630">
|
||||||
<meta property="og:image:alt" content="MVLog: journal of an unreliable narrator.">
|
<meta property="og:image:alt" content="<?=h($ogImageAlt)?>">
|
||||||
|
|
||||||
<meta name="twitter:card" content="summary_large_image">
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
<meta name="twitter:image" content="https://bubulescu.org/assets/img/bubulescuorg.jpg">
|
<meta name="twitter:image" content="<?=h($ogImageUrl)?>">
|
||||||
|
|
||||||
<link rel="alternate" type="application/rss+xml" title="MVLog RSS" href="feed.php">
|
<link rel="alternate" type="application/rss+xml" title="MVLog RSS" href="feed.php">
|
||||||
<link rel="icon" type="image/png" href="assets/img/moto_travel.png">
|
<link rel="icon" type="image/png" href="assets/img/moto_travel.png">
|
||||||
|
|
@ -385,7 +444,8 @@ $siteHeaderTitle = $headerTitles[array_rand($headerTitles)];
|
||||||
<?php foreach($slice as $v):
|
<?php foreach($slice as $v):
|
||||||
$name = basename($v);
|
$name = basename($v);
|
||||||
$base = pathinfo($name, PATHINFO_FILENAME);
|
$base = pathinfo($name, PATHINFO_FILENAME);
|
||||||
$permalinkUrl = query_url(['id' => $base]);
|
$articleIdForVideo = video_article_id($config, $name);
|
||||||
|
$permalinkUrl = $articleIdForVideo !== '' ? query_url(['id' => $articleIdForVideo]) : '';
|
||||||
$url = $config['public_videos'].'/'.rawurlencode($name);
|
$url = $config['public_videos'].'/'.rawurlencode($name);
|
||||||
$m = $videoCache[$name]['metadata'] ?? video_metadata($v);
|
$m = $videoCache[$name]['metadata'] ?? video_metadata($v);
|
||||||
$posterUrl = video_thumb_public_url($config, $name);
|
$posterUrl = video_thumb_public_url($config, $name);
|
||||||
|
|
@ -394,7 +454,7 @@ $siteHeaderTitle = $headerTitles[array_rand($headerTitles)];
|
||||||
?>
|
?>
|
||||||
<article class="video-row">
|
<article class="video-row">
|
||||||
<div class="video-row-head video-row-head-mobile">
|
<div class="video-row-head video-row-head-mobile">
|
||||||
<h2 class="video-row-title"><a href="<?=h($permalinkUrl)?>"><?=h($m['title'])?></a></h2>
|
<h2 class="video-row-title"><?php if($permalinkUrl !== ''): ?><a href="<?=h($permalinkUrl)?>"><?=h($m['title'])?></a><?php else: ?><?=h($m['title'])?><?php endif; ?></h2>
|
||||||
<p class="meta">
|
<p class="meta">
|
||||||
<span><a href="<?=h($dateFilterUrl)?>"><?=h($m['date'])?></a></span>
|
<span><a href="<?=h($dateFilterUrl)?>"><?=h($m['date'])?></a></span>
|
||||||
<?php if(!empty($m['location'])): ?><span><a href="<?=h($locFilterUrl)?>"><?=h($m['location'])?></a></span><?php endif; ?>
|
<?php if(!empty($m['location'])): ?><span><a href="<?=h($locFilterUrl)?>"><?=h($m['location'])?></a></span><?php endif; ?>
|
||||||
|
|
@ -412,7 +472,7 @@ $siteHeaderTitle = $headerTitles[array_rand($headerTitles)];
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
<div class="video-info">
|
<div class="video-info">
|
||||||
<h2 class="video-row-title video-row-title-desktop"><a href="<?=h($permalinkUrl)?>"><?=h($m['title'])?></a></h2>
|
<h2 class="video-row-title video-row-title-desktop"><?php if($permalinkUrl !== ''): ?><a href="<?=h($permalinkUrl)?>"><?=h($m['title'])?></a><?php else: ?><?=h($m['title'])?><?php endif; ?></h2>
|
||||||
<p class="meta meta-desktop">
|
<p class="meta meta-desktop">
|
||||||
<span><a href="<?=h($dateFilterUrl)?>"><?=h($m['date'])?></a></span>
|
<span><a href="<?=h($dateFilterUrl)?>"><?=h($m['date'])?></a></span>
|
||||||
<?php if(!empty($m['location'])): ?><span><a href="<?=h($locFilterUrl)?>"><?=h($m['location'])?></a></span><?php endif; ?>
|
<?php if(!empty($m['location'])): ?><span><a href="<?=h($locFilterUrl)?>"><?=h($m['location'])?></a></span><?php endif; ?>
|
||||||
|
|
@ -425,6 +485,7 @@ $siteHeaderTitle = $headerTitles[array_rand($headerTitles)];
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<?php if ($articleId === ''): ?>
|
||||||
<div class="pages">
|
<div class="pages">
|
||||||
<?php for($i=1;$i<=$pages;$i++):
|
<?php for($i=1;$i<=$pages;$i++):
|
||||||
$linkParams = $baseParams;
|
$linkParams = $baseParams;
|
||||||
|
|
@ -433,6 +494,7 @@ $siteHeaderTitle = $headerTitles[array_rand($headerTitles)];
|
||||||
<a class="<?=$i===$page?'active':''?>" href="<?=h(query_url($linkParams))?>"><?=$i?></a>
|
<a class="<?=$i===$page?'active':''?>" href="<?=h(query_url($linkParams))?>"><?=$i?></a>
|
||||||
<?php endfor; ?>
|
<?php endfor; ?>
|
||||||
</div>
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
<?php include __DIR__ . '/_footer.php'; ?>
|
<?php include __DIR__ . '/_footer.php'; ?>
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,11 @@ function try_send_show_notification($articleRefOrJob, $jobdir, $mvlog_root = nul
|
||||||
$job = basename((string)$jobdir);
|
$job = basename((string)$jobdir);
|
||||||
$articleRef = strtolower(trim((string)$articleRefOrJob));
|
$articleRef = strtolower(trim((string)$articleRefOrJob));
|
||||||
$articleId = mvlog_article_id_is_valid($articleRef) ? $articleRef : mvlog_read_article_id((string)$jobdir);
|
$articleId = mvlog_article_id_is_valid($articleRef) ? $articleRef : mvlog_read_article_id((string)$jobdir);
|
||||||
$identity = $articleId !== '' ? $articleId : $job;
|
if ($articleId === '') {
|
||||||
|
error_log("[mvlog] article id missing, skipping push for $job\n", 3, $logfile);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$identity = $articleId;
|
||||||
|
|
||||||
// Don't notify for hidden jobs
|
// Don't notify for hidden jobs
|
||||||
if (is_file($jobdir . '/.mvlog-hidden')) {
|
if (is_file($jobdir . '/.mvlog-hidden')) {
|
||||||
|
|
@ -169,14 +173,12 @@ function try_send_show_notification($articleRefOrJob, $jobdir, $mvlog_root = nul
|
||||||
|
|
||||||
if ($articleTitle === '') $articleTitle = mvlog_nice_title((string)$job);
|
if ($articleTitle === '') $articleTitle = mvlog_nice_title((string)$job);
|
||||||
|
|
||||||
$filterUrl = 'https://bubulescu.org/?' . http_build_query([
|
$articleUrl = './?id=' . rawurlencode($articleId);
|
||||||
'q' => $articleTitle,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$payload = json_encode([
|
$payload = json_encode([
|
||||||
'title' => $articleTitle,
|
'title' => $articleTitle,
|
||||||
'body' => 'New aticle @ MVL!',
|
'body' => 'New aticle @ MVL!',
|
||||||
'url' => $filterUrl,
|
'url' => $articleUrl,
|
||||||
'tag' => "mvlog-show-{$identity}",
|
'tag' => "mvlog-show-{$identity}",
|
||||||
], JSON_UNESCAPED_UNICODE);
|
], JSON_UNESCAPED_UNICODE);
|
||||||
|
|
||||||
|
|
|
||||||
329
new.php
329
new.php
|
|
@ -48,17 +48,160 @@ function ascii_safe($s){
|
||||||
function slugify($s){ $s = ascii_safe($s); $s = strtolower(trim($s)); $s = preg_replace('/[^a-z0-9]+/', '-', $s); return trim($s, '-') ?: 'movie'; }
|
function slugify($s){ $s = ascii_safe($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 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 apply_thumbnail_overlay($imagePath, $title, $teaser) {
|
function apply_thumbnail_overlay($imagePath, $title, $teaser) {
|
||||||
|
$logFile = __DIR__ . '/logs/image_magick.log';
|
||||||
|
file_put_contents($logFile, "--- New Thumbnail Job ---\n", FILE_APPEND);
|
||||||
|
|
||||||
$fontPath = __DIR__ . '/assets/fonts/IBMPlexSans-SemiBold.ttf';
|
$fontPath = __DIR__ . '/assets/fonts/IBMPlexSans-SemiBold.ttf';
|
||||||
$cmd = sprintf(
|
$titleFontPath = is_file(__DIR__ . '/assets/fonts/BebasNeue-Regular.ttf')
|
||||||
'convert %s -font %s -pointsize 30 -fill "#F3F4F6" -gravity northwest -annotate +20+20 %s -font %s -pointsize 20 -fill "#EADFC9" -gravity northwest -annotate +20+60 %s %s',
|
? __DIR__ . '/assets/fonts/BebasNeue-Regular.ttf'
|
||||||
escapeshellarg($imagePath),
|
: $fontPath;
|
||||||
escapeshellarg($fontPath),
|
$teaserFontPath = is_file(__DIR__ . '/assets/fonts/NotoSans-Bold.ttf')
|
||||||
escapeshellarg($title),
|
? __DIR__ . '/assets/fonts/NotoSans-Bold.ttf'
|
||||||
escapeshellarg($fontPath),
|
: $fontPath;
|
||||||
escapeshellarg($teaser),
|
if (!is_file($fontPath)) {
|
||||||
escapeshellarg($imagePath)
|
throw new RuntimeException('Thumbnail font file is missing.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$imgW = 1200;
|
||||||
|
$imgH = 630;
|
||||||
|
|
||||||
|
$normalize = static function ($text): string {
|
||||||
|
$text = trim((string)$text);
|
||||||
|
if ($text === '') return '';
|
||||||
|
$text = preg_replace('/\s+/u', ' ', $text) ?? $text;
|
||||||
|
return trim($text);
|
||||||
|
};
|
||||||
|
$wrapText = static function ($text, int $width) use ($normalize): string {
|
||||||
|
$text = $normalize($text);
|
||||||
|
return $text === '' ? '' : wordwrap($text, $width, "\n", false);
|
||||||
|
};
|
||||||
|
$clampWrappedText = static function ($text, int $width, int $maxLines, bool $cutLongWords = false) use ($normalize): string {
|
||||||
|
$text = $normalize($text);
|
||||||
|
if ($text === '') return '';
|
||||||
|
$words = preg_split('/\s+/u', $text, -1, PREG_SPLIT_NO_EMPTY) ?: [];
|
||||||
|
if (!$words) return '';
|
||||||
|
$candidate = implode(' ', $words);
|
||||||
|
while (true) {
|
||||||
|
$wrapped = wordwrap($candidate, $width, "\n", $cutLongWords);
|
||||||
|
$lineCount = substr_count($wrapped, "\n") + 1;
|
||||||
|
if ($lineCount <= $maxLines || count($words) <= 1) return $wrapped;
|
||||||
|
array_pop($words);
|
||||||
|
$candidate = implode(' ', $words) . '…';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
$fitTitleText = static function ($text) use ($wrapText): array {
|
||||||
|
$bestText = '';
|
||||||
|
$bestSize = 95;
|
||||||
|
$bestWidth = 28;
|
||||||
|
$bestLines = 1;
|
||||||
|
for ($size = 84; $size >= 54; $size -= 4) {
|
||||||
|
$width = max(16, min(46, (int)round(23 + (84 - $size) / 2.7)));
|
||||||
|
$candidate = $wrapText($text, $width);
|
||||||
|
$lines = substr_count($candidate, "\n") + 1;
|
||||||
|
$bestText = $candidate;
|
||||||
|
$bestSize = $size;
|
||||||
|
$bestWidth = $width;
|
||||||
|
$bestLines = $lines;
|
||||||
|
if ($lines <= 2) break;
|
||||||
|
}
|
||||||
|
return [$bestText, $bestSize, $bestWidth, $bestLines];
|
||||||
|
};
|
||||||
|
|
||||||
|
[$titleText, $titleFontSize, $titleWrapWidth, $titleLines] = $fitTitleText($title);
|
||||||
|
$teaserText = $clampWrappedText($teaser, 34, 6, true);
|
||||||
|
$teaserLines = max(1, substr_count($teaserText, "\n") + 1);
|
||||||
|
|
||||||
|
// Move the whole block a bit left and higher while keeping left-aligned text.
|
||||||
|
$left = max(14, (int)round($imgW * 0.046));
|
||||||
|
$titleY = max(20, (int)round($imgH * 0.062));
|
||||||
|
$titleLineStep = max(68, (int)round($imgH * 0.043));
|
||||||
|
$teaserY = max((int)round($imgH * 0.235), $titleY + ($titleLines * $titleLineStep) + max(54, (int)round($imgH * 0.03)));
|
||||||
|
|
||||||
|
$overlayBase = tempnam(sys_get_temp_dir(), 'thumb_overlay_');
|
||||||
|
$outputPath = dirname($imagePath) . '/' . basename($imagePath) . '.tmp.' . uniqid('', true) . '.jpg';
|
||||||
|
if ($overlayBase === false) {
|
||||||
|
throw new RuntimeException('Failed to create temporary thumbnail overlay file.');
|
||||||
|
}
|
||||||
|
$overlayPath = $overlayBase . '.png';
|
||||||
|
@unlink($overlayBase);
|
||||||
|
|
||||||
|
$cmd1 = sprintf(
|
||||||
|
'/usr/bin/convert -size %dx%d xc:none -font %s -pointsize %d -fill "#F3F4F6" -stroke black -strokewidth 2 -interline-spacing 2 -gravity northwest -annotate +%d+%d %s -font %s -pointsize 58 -fill "#EADFC9" -stroke black -strokewidth 2 -interline-spacing -18 -gravity northwest -annotate +%d+%d %s %s 2>&1',
|
||||||
|
$imgW,
|
||||||
|
$imgH,
|
||||||
|
escapeshellarg($titleFontPath),
|
||||||
|
$titleFontSize,
|
||||||
|
$left,
|
||||||
|
$titleY,
|
||||||
|
escapeshellarg($titleText),
|
||||||
|
escapeshellarg($teaserFontPath),
|
||||||
|
$left,
|
||||||
|
$teaserY,
|
||||||
|
escapeshellarg($teaserText),
|
||||||
|
escapeshellarg($overlayPath)
|
||||||
);
|
);
|
||||||
shell_exec($cmd);
|
file_put_contents($logFile, "Overlay CMD: " . $cmd1 . "\n", FILE_APPEND);
|
||||||
|
$out1 = shell_exec($cmd1);
|
||||||
|
file_put_contents($logFile, "Overlay Output: " . $out1 . "\n", FILE_APPEND);
|
||||||
|
if (!is_file($overlayPath) || filesize($overlayPath) < 100) {
|
||||||
|
@unlink($overlayPath);
|
||||||
|
@unlink($outputPath);
|
||||||
|
throw new RuntimeException('Failed to render thumbnail overlay: ' . trim((string)$out1));
|
||||||
|
}
|
||||||
|
|
||||||
|
$maxBytes = 600 * 1024;
|
||||||
|
$qualities = [88, 84, 80, 76, 72, 68, 64, 60, 56, 52, 48];
|
||||||
|
$finalPath = '';
|
||||||
|
$finalSize = 0;
|
||||||
|
$finalOut = '';
|
||||||
|
|
||||||
|
foreach ($qualities as $quality) {
|
||||||
|
$attemptPath = dirname($imagePath) . '/' . basename($imagePath) . '.tmp.' . uniqid('', true) . '.jpg';
|
||||||
|
$cmd2 = sprintf(
|
||||||
|
'/usr/bin/convert -filter Lanczos -define filter:blur=0.85 %s -resize %dx%d^ -gravity center -extent %dx%d -modulate 70,82 %s -composite -strip -interlace Plane -sampling-factor 4:2:0 -quality %d %s 2>&1',
|
||||||
|
escapeshellarg($imagePath),
|
||||||
|
$imgW,
|
||||||
|
$imgH,
|
||||||
|
$imgW,
|
||||||
|
$imgH,
|
||||||
|
escapeshellarg($overlayPath),
|
||||||
|
$quality,
|
||||||
|
escapeshellarg($attemptPath)
|
||||||
|
);
|
||||||
|
file_put_contents($logFile, "Composite CMD (q=$quality): " . $cmd2 . "\n", FILE_APPEND);
|
||||||
|
$out2 = shell_exec($cmd2);
|
||||||
|
file_put_contents($logFile, "Composite Output (q=$quality): " . $out2 . "\n", FILE_APPEND);
|
||||||
|
|
||||||
|
if (!is_file($attemptPath) || filesize($attemptPath) < 100) {
|
||||||
|
@unlink($attemptPath);
|
||||||
|
$finalOut = trim((string)$out2);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$finalPath = $attemptPath;
|
||||||
|
$finalSize = filesize($attemptPath);
|
||||||
|
$finalOut = trim((string)$out2);
|
||||||
|
if ($finalSize <= $maxBytes) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($finalPath === '') {
|
||||||
|
@unlink($overlayPath);
|
||||||
|
@unlink($outputPath);
|
||||||
|
throw new RuntimeException('Failed to compose thumbnail overlay: ' . $finalOut);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($finalSize > $maxBytes) {
|
||||||
|
file_put_contents($logFile, "Thumbnail still above size limit: " . $finalSize . " bytes\n", FILE_APPEND);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!@rename($finalPath, $imagePath)) {
|
||||||
|
@unlink($overlayPath);
|
||||||
|
@unlink($finalPath);
|
||||||
|
throw new RuntimeException('Failed to replace thumbnail image.');
|
||||||
|
}
|
||||||
|
|
||||||
|
@unlink($overlayPath);
|
||||||
|
@unlink($outputPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
function input_dirs($base){ return glob($base.'/*', GLOB_ONLYDIR) ?: []; }
|
function input_dirs($base){ return glob($base.'/*', GLOB_ONLYDIR) ?: []; }
|
||||||
|
|
@ -585,16 +728,39 @@ function is_image_media_file($name){
|
||||||
$ext = strtolower(pathinfo((string)$name, PATHINFO_EXTENSION));
|
$ext = strtolower(pathinfo((string)$name, PATHINFO_EXTENSION));
|
||||||
return in_array($ext, ['jpg','jpeg','png','webp','gif'], true);
|
return in_array($ext, ['jpg','jpeg','png','webp','gif'], true);
|
||||||
}
|
}
|
||||||
|
function public_url_path($path){
|
||||||
|
$path = str_replace('\\', '/', trim((string)$path));
|
||||||
|
if ($path === '') return '';
|
||||||
|
$parts = array_values(array_filter(explode('/', $path), static fn($p) => $p !== ''));
|
||||||
|
return implode('/', array_map('rawurlencode', $parts));
|
||||||
|
}
|
||||||
function video_thumb_public_url($config, $videoName){
|
function video_thumb_public_url($config, $videoName){
|
||||||
$videoName = basename((string)$videoName);
|
$videoName = basename((string)$videoName);
|
||||||
if ($videoName === '') return '';
|
if ($videoName === '') return '';
|
||||||
$base = pathinfo($videoName, PATHINFO_FILENAME);
|
$info = video_thumb_source_info($config, $videoName);
|
||||||
foreach (['jpg','jpeg','png','webp','gif'] as $ext) {
|
$sourcePath = (string)($info['source_path'] ?? '');
|
||||||
$file = $config['videos_dir'] . '/' . $base . '_thumb.' . $ext;
|
if ($sourcePath !== '') {
|
||||||
if (is_file($file)) return $config['public_videos'] . '/' . rawurlencode($base . '_thumb.' . $ext) . '?v=' . filemtime($file);
|
$abs = str_starts_with($sourcePath, '/') ? $sourcePath : __DIR__ . '/' . ltrim($sourcePath, '/');
|
||||||
|
if (is_file($abs)) return public_url_path($sourcePath) . '?v=' . filemtime($abs);
|
||||||
|
}
|
||||||
|
$sourceFile = basename((string)($info['source_file'] ?? ''));
|
||||||
|
$inputDir = basename((string)($info['input_dir'] ?? ''));
|
||||||
|
if ($sourceFile !== '' && $inputDir !== '') {
|
||||||
|
$rel = 'in-dir/' . $inputDir . '/' . $sourceFile;
|
||||||
|
$abs = __DIR__ . '/' . $rel;
|
||||||
|
if (is_file($abs)) return public_url_path($rel) . '?v=' . filemtime($abs);
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
function video_thumb_source_info($config, $videoName){
|
||||||
|
$videoName = basename((string)$videoName);
|
||||||
|
if ($videoName === '') return [];
|
||||||
|
$base = pathinfo($videoName, PATHINFO_FILENAME);
|
||||||
|
$file = $config['videos_dir'] . '/' . $base . '_thumb.json';
|
||||||
|
if (!is_file($file)) return [];
|
||||||
|
$data = json_decode((string)file_get_contents($file), true);
|
||||||
|
return is_array($data) ? $data : [];
|
||||||
|
}
|
||||||
function ensure_state_article_id($dir){
|
function ensure_state_article_id($dir){
|
||||||
$articleId = ensure_article_id($dir);
|
$articleId = ensure_article_id($dir);
|
||||||
$stateFile = $dir . '/.movmaker-state.json';
|
$stateFile = $dir . '/.movmaker-state.json';
|
||||||
|
|
@ -647,9 +813,13 @@ function input_dir_output($dir){
|
||||||
return basename((string)($state['output'] ?? ''));
|
return basename((string)($state['output'] ?? ''));
|
||||||
}
|
}
|
||||||
function regenerate_input_dir_maps($dir, $config){
|
function regenerate_input_dir_maps($dir, $config){
|
||||||
|
$logFile = '/var/log/mvlog_map.log';
|
||||||
$python = trim((string)shell_exec('command -v python3 2>/dev/null'));
|
$python = trim((string)shell_exec('command -v python3 2>/dev/null'));
|
||||||
$script = __DIR__ . '/bin/gpsmap.py';
|
$script = __DIR__ . '/bin/gpsmap.py';
|
||||||
if ($python === '' || !is_file($script)) return;
|
if ($python === '' || !is_file($script)) {
|
||||||
|
file_put_contents($logFile, '[mvlog] map regenerate skipped for ' . basename((string)$dir) . ' (python=' . ($python !== '' ? 'yes' : 'no') . ', script=' . (is_file($script) ? 'yes' : 'no') . ")\n", FILE_APPEND);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$state = input_dir_state($dir);
|
$state = input_dir_state($dir);
|
||||||
$targets = [];
|
$targets = [];
|
||||||
|
|
@ -665,14 +835,19 @@ function regenerate_input_dir_maps($dir, $config){
|
||||||
}
|
}
|
||||||
|
|
||||||
$targets = array_values(array_unique($targets));
|
$targets = array_values(array_unique($targets));
|
||||||
|
file_put_contents($logFile, '[mvlog] map regenerate requested for ' . basename((string)$dir) . ' targets=' . implode(', ', array_map('basename', $targets)) . "\n", FILE_APPEND);
|
||||||
|
|
||||||
foreach ($targets as $outPng) {
|
foreach ($targets as $outPng) {
|
||||||
|
file_put_contents($logFile, '[mvlog] map regenerate start for ' . basename((string)$dir) . ' target=' . basename((string)$outPng) . "\n", FILE_APPEND);
|
||||||
$cmd = escapeshellarg($python) . ' ' . escapeshellarg($script) . ' ' . escapeshellarg($dir) . ' ' . escapeshellarg($outPng) . ' 2>&1';
|
$cmd = escapeshellarg($python) . ' ' . escapeshellarg($script) . ' ' . escapeshellarg($dir) . ' ' . escapeshellarg($outPng) . ' 2>&1';
|
||||||
$output = (string)shell_exec($cmd);
|
$output = (string)shell_exec($cmd);
|
||||||
if (is_file($outPng)) {
|
if (is_file($outPng)) {
|
||||||
@chmod($outPng, 0664);
|
@chmod($outPng, 0664);
|
||||||
|
clearstatcache(true, $outPng);
|
||||||
|
$size = @filesize($outPng);
|
||||||
|
file_put_contents($logFile, '[mvlog] map regenerate done for ' . basename((string)$dir) . ' target=' . basename((string)$outPng) . ' bytes=' . ($size !== false ? $size : 'unknown') . "\n", FILE_APPEND);
|
||||||
} else {
|
} else {
|
||||||
error_log('[mvlog] map regenerate failed for '.basename((string)$dir).' target='.basename((string)$outPng).' output='.trim($output)."\n", 3, '/var/log/mvlog_map.log');
|
file_put_contents($logFile, '[mvlog] map regenerate failed for ' . basename((string)$dir) . ' target=' . basename((string)$outPng) . ' output=' . trim($output) . "\n", FILE_APPEND);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -860,6 +1035,9 @@ try {
|
||||||
header('Location: new.php?tab=edit&msg=' . rawurlencode($message));
|
header('Location: new.php?tab=edit&msg=' . rawurlencode($message));
|
||||||
exit;
|
exit;
|
||||||
} elseif ($action === 'set_video_thumb') {
|
} elseif ($action === 'set_video_thumb') {
|
||||||
|
$logFile = __DIR__ . '/logs/image_magick.log';
|
||||||
|
file_put_contents($logFile, "Entering set_video_thumb action...\n", FILE_APPEND); // LOG 1
|
||||||
|
|
||||||
$dir = resolve_input_dir_from_request($config['uploads_dir'], $_POST['id'] ?? '', $_POST['dir'] ?? '', $articleIndex);
|
$dir = resolve_input_dir_from_request($config['uploads_dir'], $_POST['id'] ?? '', $_POST['dir'] ?? '', $articleIndex);
|
||||||
$articleId = ensure_state_article_id($dir);
|
$articleId = ensure_state_article_id($dir);
|
||||||
if (input_dir_running($dir)) throw new RuntimeException('This input directory is being rendered. Editing is disabled until the job completes.');
|
if (input_dir_running($dir)) throw new RuntimeException('This input directory is being rendered. Editing is disabled until the job completes.');
|
||||||
|
|
@ -876,6 +1054,19 @@ try {
|
||||||
if (!$outputs) throw new RuntimeException('No rendered video found for this input directory yet.');
|
if (!$outputs) throw new RuntimeException('No rendered video found for this input directory yet.');
|
||||||
|
|
||||||
$src = $dir . '/' . $file;
|
$src = $dir . '/' . $file;
|
||||||
|
$thumbName = $articleId . '_og.jpg';
|
||||||
|
$target = $config['videos_dir'] . '/' . $thumbName;
|
||||||
|
if (is_file($target)) @unlink($target);
|
||||||
|
if (!@copy($src, $target)) {
|
||||||
|
file_put_contents($logFile, "Failed to copy thumbnail.\n", FILE_APPEND); // LOG 2
|
||||||
|
throw new RuntimeException('Failed to write thumbnail.');
|
||||||
|
}
|
||||||
|
@chmod($target, 0664);
|
||||||
|
|
||||||
|
file_put_contents($logFile, "Thumbnail copied, now applying overlay...\n", FILE_APPEND); // LOG 3
|
||||||
|
|
||||||
|
// Apply text overlay after smart resizing/cropping to 1200x630
|
||||||
|
apply_thumbnail_overlay($target, (string)($meta['title'] ?? ''), (string)($meta['teaser'] ?? ''));
|
||||||
$written = [];
|
$written = [];
|
||||||
foreach ($outputs as $out) {
|
foreach ($outputs as $out) {
|
||||||
$base = pathinfo($out, PATHINFO_FILENAME);
|
$base = pathinfo($out, PATHINFO_FILENAME);
|
||||||
|
|
@ -884,15 +1075,20 @@ try {
|
||||||
$old = $config['thumbs_dir'] . '/' . $base . '.' . $oldExt;
|
$old = $config['thumbs_dir'] . '/' . $base . '.' . $oldExt;
|
||||||
if (is_file($old)) @unlink($old);
|
if (is_file($old)) @unlink($old);
|
||||||
}
|
}
|
||||||
|
$thumbMeta = [
|
||||||
// Copy new thumbnail to videos_dir with _thumb.jpg suffix
|
'article_id' => $articleId,
|
||||||
$target = $config['videos_dir'] . '/' . $base . '_thumb.jpg';
|
'input_dir' => basename($dir),
|
||||||
if (!@copy($src, $target)) throw new RuntimeException('Failed to write thumbnail.');
|
'source_file' => $file,
|
||||||
@chmod($target, 0664);
|
'source_path' => 'in-dir/' . basename($dir) . '/' . $file,
|
||||||
|
'thumb_file' => $thumbName,
|
||||||
// Apply text overlay
|
'thumb_path' => 'out-dir/' . $thumbName,
|
||||||
apply_thumbnail_overlay($target, $meta['title'], $meta['teaser']);
|
'created_at' => gmdate('c'),
|
||||||
|
];
|
||||||
|
$thumbMetaPath = $config['videos_dir'] . '/' . $base . '_thumb.json';
|
||||||
|
if (file_put_contents($thumbMetaPath, json_encode($thumbMeta, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n", LOCK_EX) === false) {
|
||||||
|
throw new RuntimeException('Failed to write thumbnail metadata.');
|
||||||
|
}
|
||||||
|
@chmod($thumbMetaPath, 0664);
|
||||||
$written[] = basename($target);
|
$written[] = basename($target);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -912,11 +1108,15 @@ try {
|
||||||
$dir = resolve_input_dir_from_request($config['uploads_dir'], $_POST['id'] ?? '', $_POST['dir'] ?? '', $articleIndex);
|
$dir = resolve_input_dir_from_request($config['uploads_dir'], $_POST['id'] ?? '', $_POST['dir'] ?? '', $articleIndex);
|
||||||
$articleId = ensure_state_article_id($dir);
|
$articleId = ensure_state_article_id($dir);
|
||||||
if (input_dir_running($dir)) throw new RuntimeException('This input directory is being rendered. Editing is disabled until the job completes.');
|
if (input_dir_running($dir)) throw new RuntimeException('This input directory is being rendered. Editing is disabled until the job completes.');
|
||||||
write_data($dir, $_POST);
|
file_put_contents('/var/log/mvlog_map.log', '[mvlog] save changes pressed for ' . basename($dir) . ' id=' . $articleId . "\n", FILE_APPEND);
|
||||||
save_uploads('media', $dir, $allowedMedia);
|
try {
|
||||||
save_uploads('audio', $dir, $allowedAudio);
|
write_data($dir, $_POST);
|
||||||
// Always regenerate map image(s) on Save changes.
|
save_uploads('media', $dir, $allowedMedia);
|
||||||
regenerate_input_dir_maps($dir, $config);
|
save_uploads('audio', $dir, $allowedAudio);
|
||||||
|
} finally {
|
||||||
|
// Always regenerate map image(s) whenever Save changes is pressed.
|
||||||
|
regenerate_input_dir_maps($dir, $config);
|
||||||
|
}
|
||||||
if (!empty($_POST['ajax'])) ajax_json(['ok'=>true, 'message'=>'Updated input directory: in-dir/' . basename($dir), 'dir'=>basename($dir), 'id'=>$articleId]);
|
if (!empty($_POST['ajax'])) ajax_json(['ok'=>true, 'message'=>'Updated input directory: in-dir/' . basename($dir), 'dir'=>basename($dir), 'id'=>$articleId]);
|
||||||
header('Location: new.php?tab=edit');
|
header('Location: new.php?tab=edit');
|
||||||
exit;
|
exit;
|
||||||
|
|
@ -1067,7 +1267,7 @@ $runningJobs = active_worker_jobs($config);
|
||||||
<?php if($err): ?><div class="err"><?=h($err)?></div><?php endif; ?>
|
<?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?') 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==='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><form method="get" class="admin-search-form"><input type="hidden" name="tab" value="edit"><div class="admin-search-wrap"><input name="q" value="<?=h($rawKeyword)?>" placeholder="Search title/teaser/quote_da/description or use date=... / location=..." aria-label="Search input dirs"><a class="admin-search-clear <?=$searchEmpty ? 'is-empty' : ''?>" href="new.php?tab=edit" aria-label="Clear filter" title="Clear filter">×</a></div><div class="admin-search-filters"><label class="admin-filter-item" title="Preview"><input type="checkbox" name="f_preview" value="1" <?=$filterPreview?'checked':''?>><span>P</span></label><label class="admin-filter-item" title="Render"><input type="checkbox" name="f_render" value="1" <?=$filterRender?'checked':''?>><span>R</span></label><label class="admin-filter-item" title="Show"><input type="checkbox" name="f_shown" value="1" <?=$filterShown?'checked':''?>><span>S</span></label></div><button type="submit" class="admin-search-submit" aria-label="Filter"><span class="icon" aria-hidden="true">🔍</span><span class="sr-only">Filter</span></button></form><?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 : ''; $posterUrl=$hasVideo ? video_thumb_public_url($config, $displayOutput) : ''; $previewVideo=$hasVideo && $displayOutput === $previewOutput; $canShow=$hasFullVideo && !$preview; $visible=$canShow && input_dir_visible($d); $mapHidden=input_dir_map_hidden($d); $info=$dirInfo[basename($d)] ?? input_dir_info($d); $data=read_data($d); $meta=['title'=>(string)($data['title'] ?? ''),'teaser'=>(string)($data['teaser'] ?? ''),'date'=>(string)($data['date'] ?? ''),'location'=>(string)($data['place'] ?? ''),'quote_da'=>(string)($data['quote_da'] ?? ''),'description'=>(string)($data['description'] ?? '')]; $currentSignature=$info['signature'] ?? ''; $fullFingerprint=is_array($state)?(string)($state['fingerprint'] ?? ''):''; $previewFingerprint=is_array($state)?(string)($state['preview_fingerprint'] ?? ''):''; $editedSinceRender=$hasFullVideo && $currentSignature !== '' && $fullFingerprint !== '' && $currentSignature !== $fullFingerprint; $editedSincePreview=!$hasFullVideo && $previewVideo && $currentSignature !== '' && $previewFingerprint !== '' && $currentSignature !== $previewFingerprint; $staleLabel=$editedSinceRender ? 'Edited since render' : ($editedSincePreview ? 'Edited since preview' : ''); ?><article class="video-row admin-video-row<?= $visible ? ' shown' : '' ?>" data-job="<?=h(basename($d))?>" data-id="<?=h(read_article_id($d))?>"><div><?php if($hasVideo): ?><div class="video-wrapper"><video controls preload="metadata" src="<?=h($config['public_videos'].'/'.rawurlencode($displayOutput))?>"<?= $posterUrl !== '' ? ' poster="'.h($posterUrl).'"' : '' ?>></video><?php $map_n = ($displayOutput !== '' ? pathinfo($displayOutput, PATHINFO_FILENAME) : basename($d)) . '_map.png'; if(is_file($config['videos_dir'].'/'.$map_n)): ?><a class="map-image-link" href="<?=h($config['public_videos'].'/'.rawurlencode($map_n))?>"><img class="map-image" src="<?=h($config['public_videos'].'/'.rawurlencode($map_n))?>" alt="Map"></a><?php endif; ?></div><?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">
|
<?php if($tab==='edit' && !$editDir): ?><section><form method="get" class="admin-search-form"><input type="hidden" name="tab" value="edit"><div class="admin-search-wrap"><input name="q" value="<?=h($rawKeyword)?>" placeholder="Search title/teaser/quote_da/description or use date=... / location=..." aria-label="Search input dirs"><a class="admin-search-clear <?=$searchEmpty ? 'is-empty' : ''?>" href="new.php?tab=edit" aria-label="Clear filter" title="Clear filter">×</a></div><div class="admin-search-filters"><label class="admin-filter-item" title="Preview"><input type="checkbox" name="f_preview" value="1" <?=$filterPreview?'checked':''?>><span>P</span></label><label class="admin-filter-item" title="Render"><input type="checkbox" name="f_render" value="1" <?=$filterRender?'checked':''?>><span>R</span></label><label class="admin-filter-item" title="Show"><input type="checkbox" name="f_shown" value="1" <?=$filterShown?'checked':''?>><span>S</span></label></div><button type="submit" class="admin-search-submit" aria-label="Filter"><span class="icon" aria-hidden="true">🔍</span><span class="sr-only">Filter</span></button></form><?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 : ''; $posterUrl=$hasVideo ? video_thumb_public_url($config, $displayOutput) : ''; $previewVideo=$hasVideo && $displayOutput === $previewOutput; $canShow=$hasFullVideo && !$preview; $visible=$canShow && input_dir_visible($d); $mapHidden=input_dir_map_hidden($d); $info=$dirInfo[basename($d)] ?? input_dir_info($d); $data=read_data($d); $meta=['title'=>(string)($data['title'] ?? ''),'teaser'=>(string)($data['teaser'] ?? ''),'date'=>(string)($data['date'] ?? ''),'location'=>(string)($data['place'] ?? ''),'quote_da'=>(string)($data['quote_da'] ?? ''),'description'=>(string)($data['description'] ?? '')]; $currentSignature=$info['signature'] ?? ''; $fullFingerprint=is_array($state)?(string)($state['fingerprint'] ?? ''):''; $previewFingerprint=is_array($state)?(string)($state['preview_fingerprint'] ?? ''):''; $editedSinceRender=$hasFullVideo && $currentSignature !== '' && $fullFingerprint !== '' && $currentSignature !== $fullFingerprint; $editedSincePreview=!$hasFullVideo && $previewVideo && $currentSignature !== '' && $previewFingerprint !== '' && $currentSignature !== $previewFingerprint; $staleLabel=$editedSinceRender ? 'Edited since render' : ($editedSincePreview ? 'Edited since preview' : ''); ?><article class="video-row admin-video-row<?= $visible ? ' shown' : '' ?>" data-job="<?=h(basename($d))?>" data-id="<?=h(read_article_id($d))?>"><div><?php if($hasVideo): ?><div class="video-wrapper"><video controls preload="metadata" src="<?=h($config['public_videos'].'/'.rawurlencode($displayOutput))?>"<?= $posterUrl !== '' ? ' poster="'.h($posterUrl).'"' : '' ?>></video><?php $map_n = ($displayOutput !== '' ? pathinfo($displayOutput, PATHINFO_FILENAME) : basename($d)) . '_map.png'; if(is_file($config['videos_dir'].'/'.$map_n)): ?><a class="map-image-link" href="<?=h($config['public_videos'].'/'.rawurlencode($map_n))?>"><img class="map-image" src="<?=h($config['public_videos'].'/'.rawurlencode($map_n))?>" alt="Map"></a><?php endif; ?></div><?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"><?php $thumbMeta = $hasVideo ? video_thumb_source_info($config, $displayOutput) : []; if(!empty($thumbMeta['source_file'])): ?><p style="margin:.2rem 0 .45rem;color:#A0A4AB;font-size:.82rem">Thumb source: <?=h((string)$thumbMeta['source_file'])?></p><?php endif; ?><div class="admin-row-top"><div class="admin-switches">
|
||||||
<form method="post" class="inline-form switch-form" data-switch="preview">
|
<form method="post" class="inline-form switch-form" data-switch="preview">
|
||||||
<input type="hidden" name="action" value="set_preview">
|
<input type="hidden" name="action" value="set_preview">
|
||||||
<input type="hidden" name="dir" value="<?=h(basename($d))?>">
|
<input type="hidden" name="dir" value="<?=h(basename($d))?>">
|
||||||
|
|
@ -1111,7 +1311,7 @@ $runningJobs = active_worker_jobs($config);
|
||||||
<?php if($hasVideo): ?><time class="admin-time<?= $staleLabel ? ' admin-time-stale' : '' ?>" title="<?=h($staleLabel ?: 'Video created at')?>"><?=h(date('d.m.Y H:i', filemtime($displayVideoPath)))?></time><?php endif; ?>
|
<?php if($hasVideo): ?><time class="admin-time<?= $staleLabel ? ' admin-time-stale' : '' ?>" title="<?=h($staleLabel ?: '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="?id=<?=rawurlencode(read_article_id($d))?>&edit=<?=rawurlencode(basename($d))?>" data-edit-action="1" data-edit-href="?id=<?=rawurlencode(read_article_id($d))?>&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(!empty($meta['teaser'])): ?><p class="description-teaser"><?=h((string)$meta['teaser'])?></p><?php endif; ?><?php if($meta['description']): ?><p class="description"><?=nl2br(h($meta['description']), false)?></p><?php endif; ?><?php if(!empty($meta['quote_da'])): ?><p class="description-quote">“<?=h(trim((string)$meta['quote_da'], "\"“”"))?>”</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; ?>
|
</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="?id=<?=rawurlencode(read_article_id($d))?>&edit=<?=rawurlencode(basename($d))?>" data-edit-action="1" data-edit-href="?id=<?=rawurlencode(read_article_id($d))?>&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(!empty($meta['teaser'])): ?><p class="description-teaser"><?=h((string)$meta['teaser'])?></p><?php endif; ?><?php if($meta['description']): ?><p class="description"><?=nl2br(h($meta['description']), false)?></p><?php endif; ?><?php if(!empty($meta['quote_da'])): ?><p class="description-quote">“<?=h(trim((string)$meta['quote_da'], "\"“”"))?>”</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><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" id="edit-form"><input type="hidden" name="action" value="update"><input type="hidden" name="dir" value="<?=h(basename($editDir))?>"><input type="hidden" name="id" value="<?=h(ensure_article_id($editDir))?>"><label>Title<input name="title" value="<?=h($editData['title'])?>"></label><label>Teaser<input name="teaser" value="<?=h($editData['teaser'])?>" maxlength="240"></label><label>Danish quote<input name="quote_da" value="<?=h($editData['quote_da'])?>" maxlength="280"></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)?>" controls preload="metadata"></video><a class="download-link" href="<?=h($fileUrl)?>" download>Download</a><?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><div class="file-actions" style="display:flex;gap:.45rem;flex-wrap:wrap;align-self:start"><?php if(in_array($ext,['jpg','jpeg','png','webp','gif'])): ?><button type="button" onclick="setThumb(<?=h(json_encode($f))?>)">Set as thumb</button><?php endif; ?><button type="button" onclick="deleteFile(<?=h(json_encode($f))?>)">Remove</button></div></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="id" value="<?=h(ensure_article_id($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))?>"><input type="hidden" name="id" value="<?=h(ensure_article_id($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==='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))?>"><input type="hidden" name="id" value="<?=h(ensure_article_id($editDir))?>"><label>Title<input name="title" value="<?=h($editData['title'])?>"></label><label>Teaser<input name="teaser" value="<?=h($editData['teaser'])?>" maxlength="240"></label><label>Danish quote<input name="quote_da" value="<?=h($editData['quote_da'])?>" maxlength="280"></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)?>" controls preload="metadata"></video><a class="download-link" href="<?=h($fileUrl)?>" download>Download</a><?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><div class="file-actions" style="display:flex;gap:.45rem;flex-wrap:wrap;align-self:start"><?php if(in_array($ext,['jpg','jpeg','png','webp','gif'])): ?><button type="button" class="set-thumb-btn" onclick="setThumb(<?=h(json_encode($f))?>)">Set as thumb</button><?php endif; ?><button type="button" onclick="deleteFile(<?=h(json_encode($f))?>)">Remove</button></div></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="id" value="<?=h(ensure_article_id($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))?>"><input type="hidden" name="id" value="<?=h(ensure_article_id($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>Teaser<input name="teaser" maxlength="240"></label><label>Danish quote<input name="quote_da" maxlength="280"></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==='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>Teaser<input name="teaser" maxlength="240"></label><label>Danish quote<input name="quote_da" maxlength="280"></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>
|
<script>
|
||||||
function deleteFile(name){
|
function deleteFile(name){
|
||||||
|
|
@ -1149,9 +1349,53 @@ function deleteFile(name){
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var thumbBusyOverlay = null;
|
||||||
|
function ensureThumbBusyStyles(){
|
||||||
|
if(document.getElementById('thumb-busy-styles')) return;
|
||||||
|
var style = document.createElement('style');
|
||||||
|
style.id = 'thumb-busy-styles';
|
||||||
|
style.textContent = '@keyframes mvlog-spin{to{transform:rotate(360deg)}}' +
|
||||||
|
'#thumb-busy-overlay{position:fixed;inset:0;background:rgba(17,24,39,.42);backdrop-filter:blur(1px);display:none;align-items:center;justify-content:center;z-index:2990;}' +
|
||||||
|
'#thumb-busy-overlay.show{display:flex;}' +
|
||||||
|
'#thumb-busy-overlay .panel{background:rgba(15,23,42,.92);color:#F3F4F6;border:1px solid rgba(255,255,255,.14);box-shadow:0 20px 60px rgba(0,0,0,.38);border-radius:16px;padding:22px 28px;min-width:240px;text-align:center;}' +
|
||||||
|
'#thumb-busy-overlay .spinner{width:52px;height:52px;border:5px solid rgba(255,255,255,.18);border-top-color:#F3F4F6;border-radius:50%;margin:0 auto 14px;animation:mvlog-spin .8s linear infinite;}' +
|
||||||
|
'#thumb-busy-overlay .label{font-size:1rem;font-weight:600;}' +
|
||||||
|
'button.set-thumb-btn.is-busy-target{opacity:.55;cursor:not-allowed;}';
|
||||||
|
document.head.appendChild(style);
|
||||||
|
}
|
||||||
|
function ensureThumbBusyOverlay(){
|
||||||
|
if(thumbBusyOverlay) return thumbBusyOverlay;
|
||||||
|
thumbBusyOverlay = document.getElementById('thumb-busy-overlay');
|
||||||
|
if(thumbBusyOverlay) return thumbBusyOverlay;
|
||||||
|
ensureThumbBusyStyles();
|
||||||
|
thumbBusyOverlay = document.createElement('div');
|
||||||
|
thumbBusyOverlay.id = 'thumb-busy-overlay';
|
||||||
|
thumbBusyOverlay.innerHTML = '<div class="panel"><div class="spinner"></div><div class="label">Creating thumbnail…</div></div>';
|
||||||
|
document.body.appendChild(thumbBusyOverlay);
|
||||||
|
return thumbBusyOverlay;
|
||||||
|
}
|
||||||
|
function setThumbButtonsDisabled(disabled){
|
||||||
|
var buttons = document.querySelectorAll('button.set-thumb-btn');
|
||||||
|
for(var i=0;i<buttons.length;i++){
|
||||||
|
buttons[i].disabled = disabled;
|
||||||
|
buttons[i].classList.toggle('is-busy-target', disabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function showThumbBusy(label){
|
||||||
|
var overlay = ensureThumbBusyOverlay();
|
||||||
|
setThumbButtonsDisabled(true);
|
||||||
|
var text = overlay.querySelector('.label');
|
||||||
|
if(text && label) text.textContent = label;
|
||||||
|
overlay.classList.add('show');
|
||||||
|
}
|
||||||
|
function hideThumbBusy(){
|
||||||
|
if(thumbBusyOverlay) thumbBusyOverlay.classList.remove('show');
|
||||||
|
setThumbButtonsDisabled(false);
|
||||||
|
}
|
||||||
function setThumb(name){
|
function setThumb(name){
|
||||||
var ctx = getEditDirContext();
|
var ctx = getEditDirContext();
|
||||||
if(!ctx.dir){ showToast('Internal error: missing input dir', false); return; }
|
if(!ctx.dir){ showToast('Internal error: missing input dir', false); return; }
|
||||||
|
showThumbBusy('Creating thumbnail…');
|
||||||
var body = 'action=set_video_thumb&dir=' + encodeURIComponent(ctx.dir) + '&id=' + encodeURIComponent(ctx.id) + '&file=' + encodeURIComponent(name) + '&ajax=1';
|
var body = 'action=set_video_thumb&dir=' + encodeURIComponent(ctx.dir) + '&id=' + encodeURIComponent(ctx.id) + '&file=' + encodeURIComponent(name) + '&ajax=1';
|
||||||
fetch('new.php', {
|
fetch('new.php', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
@ -1162,8 +1406,10 @@ function setThumb(name){
|
||||||
.then(function(json){
|
.then(function(json){
|
||||||
if(!json || !json.ok) throw new Error((json && (json.error || json.message)) ? (json.error || json.message) : 'Set thumb failed');
|
if(!json || !json.ok) throw new Error((json && (json.error || json.message)) ? (json.error || json.message) : 'Set thumb failed');
|
||||||
showToast(json.message || ('Thumbnail set from ' + name), true);
|
showToast(json.message || ('Thumbnail set from ' + name), true);
|
||||||
|
setTimeout(hideThumbBusy, 120);
|
||||||
}).catch(function(err){
|
}).catch(function(err){
|
||||||
showToast(err.message || String(err), false);
|
showToast(err.message || String(err), false);
|
||||||
|
setTimeout(hideThumbBusy, 120);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1386,7 +1632,7 @@ function handleAjaxFormSubmit(form, redirectTab) {
|
||||||
data.set('ajax', '1');
|
data.set('ajax', '1');
|
||||||
|
|
||||||
const action = String(data.get('action') || '');
|
const action = String(data.get('action') || '');
|
||||||
const verb = action === 'update' ? 'Saving'
|
const verb = action === 'update' ? 'Saving changes…'
|
||||||
: action === 'delete_video' ? 'Deleting'
|
: action === 'delete_video' ? 'Deleting'
|
||||||
: action === 'delete_dir' ? 'Deleting'
|
: action === 'delete_dir' ? 'Deleting'
|
||||||
: action === 'delete_file' ? 'Deleting'
|
: action === 'delete_file' ? 'Deleting'
|
||||||
|
|
@ -1395,18 +1641,22 @@ function handleAjaxFormSubmit(form, redirectTab) {
|
||||||
|
|
||||||
let timer = 0;
|
let timer = 0;
|
||||||
let success = false;
|
let success = false;
|
||||||
|
const useBusyOverlay = action === 'update';
|
||||||
if (submit) {
|
if (submit) {
|
||||||
submit.dataset.label = submit.dataset.label || submit.textContent.trim() || 'Submit';
|
submit.dataset.label = submit.dataset.label || submit.textContent.trim() || 'Submit';
|
||||||
submit.disabled = true;
|
submit.disabled = true;
|
||||||
submit.style.opacity = '0.78';
|
submit.style.opacity = '0.78';
|
||||||
submit.style.cursor = 'wait';
|
submit.style.cursor = 'wait';
|
||||||
let dots = 0;
|
if (!useBusyOverlay) {
|
||||||
submit.textContent = verb;
|
let dots = 0;
|
||||||
timer = window.setInterval(function(){
|
submit.textContent = verb;
|
||||||
dots = (dots + 1) % 4;
|
timer = window.setInterval(function(){
|
||||||
submit.textContent = verb + '.'.repeat(dots);
|
dots = (dots + 1) % 4;
|
||||||
}, 320);
|
submit.textContent = verb + '.'.repeat(dots);
|
||||||
|
}, 320);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if (useBusyOverlay) showThumbBusy('Saving changes…');
|
||||||
|
|
||||||
try{
|
try{
|
||||||
const res = await fetch('new.php', { method: 'POST', body: data, credentials: 'same-origin', headers: { 'Accept': 'application/json' }});
|
const res = await fetch('new.php', { method: 'POST', body: data, credentials: 'same-origin', headers: { 'Accept': 'application/json' }});
|
||||||
|
|
@ -1414,9 +1664,11 @@ function handleAjaxFormSubmit(form, redirectTab) {
|
||||||
if(!res.ok || !json.ok) throw new Error(json.error || 'Operation failed');
|
if(!res.ok || !json.ok) throw new Error(json.error || 'Operation failed');
|
||||||
success = true;
|
success = true;
|
||||||
showToast(json.message || 'Operation successful', true);
|
showToast(json.message || 'Operation successful', true);
|
||||||
|
if (useBusyOverlay) setTimeout(hideThumbBusy, 120);
|
||||||
setTimeout(() => { window.location.href = 'new.php?tab=' + (json.tab || redirectTab); }, 450);
|
setTimeout(() => { window.location.href = 'new.php?tab=' + (json.tab || redirectTab); }, 450);
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
showToast(e.message || 'Operation failed', false);
|
showToast(e.message || 'Operation failed', false);
|
||||||
|
if (useBusyOverlay) setTimeout(hideThumbBusy, 120);
|
||||||
} finally {
|
} finally {
|
||||||
if (timer) window.clearInterval(timer);
|
if (timer) window.clearInterval(timer);
|
||||||
if(submit && !success) {
|
if(submit && !success) {
|
||||||
|
|
@ -1425,6 +1677,7 @@ function handleAjaxFormSubmit(form, redirectTab) {
|
||||||
submit.style.cursor = '';
|
submit.style.cursor = '';
|
||||||
submit.textContent = submit.dataset.label || 'Submit';
|
submit.textContent = submit.dataset.label || 'Submit';
|
||||||
}
|
}
|
||||||
|
if (useBusyOverlay && !success) hideThumbBusy();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue