Show on main landing page restore, separately from permalink

This commit is contained in:
hbrain 2026-06-04 23:57:00 +02:00
parent 5505f0dee2
commit edcb6ca928
4 changed files with 119 additions and 19 deletions

View file

@ -6,6 +6,7 @@ foreach (["videos_dir", "thumbs_dir", "uploads_dir"] as $d) if (!is_dir($config[
if (!is_dir(__DIR__ . "/cache")) mkdir(__DIR__ . "/cache", 0775, true);
const ARTICLE_ID_FILE = '.mvlog-id';
const MAP_HIDDEN_FILE = '.mvlog-hide-map';
const PERMALINK_FILE = '.mvlog-permalink';
function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, "UTF-8"); }
function ajax_json($data){ header('Content-Type: application/json; charset=UTF-8'); echo json_encode($data, JSON_UNESCAPED_UNICODE); exit; }
function lower_text($s){ return function_exists('mb_strtolower') ? mb_strtolower((string)$s, 'UTF-8') : strtolower((string)$s); }
@ -651,7 +652,7 @@ entries = []
for dirpath, dirnames, filenames in os.walk(root):
dirnames[:] = [d for d in dirnames if d != '.movmaker-lock']
for name in filenames:
if name in {'.movmaker-state.json', '.movmaker-enabled', '.movmaker-preview', '.mvlog-hidden', '.mvlog-id', '.mvlog-hide-map'}:
if name in {'.movmaker-state.json', '.movmaker-enabled', '.movmaker-preview', '.mvlog-hidden', '.mvlog-permalink', '.mvlog-id', '.mvlog-hide-map'}:
continue
path = os.path.join(dirpath, name)
rel = os.path.relpath(path, root)
@ -763,6 +764,17 @@ function set_input_dir_visible($dir, $visible){
if ($visible) { if (is_file($path)) unlink($path); }
else { file_put_contents($path, "hidden\n"); @chmod($path, 0664); }
}
function input_dir_permalink($dir){ return is_file($dir . '/' . PERMALINK_FILE); }
function input_dir_can_permalink($dir, $config){
$state = input_dir_state($dir);
$output = basename((string)($state['output'] ?? ''));
return $output !== '' && is_file($config['videos_dir'] . '/' . $output) && !input_dir_preview($dir);
}
function set_input_dir_permalink($dir, $permalink){
$path = $dir . '/' . PERMALINK_FILE;
if ($permalink) { file_put_contents($path, "permalink\n"); @chmod($path, 0664); }
elseif (is_file($path)) unlink($path);
}
function input_dir_map_hidden($dir){ return is_file($dir . '/' . MAP_HIDDEN_FILE); }
function set_input_dir_map_hidden($dir, $hidden){
$path = $dir . '/' . MAP_HIDDEN_FILE;
@ -999,6 +1011,7 @@ try {
'preview' => input_dir_preview($dir),
'visible' => input_dir_visible($dir) && $canShow,
'can_show' => $canShow,
'can_permalink' => input_dir_can_permalink($dir, $config),
'dir' => basename($dir),
'id' => $articleId,
'message' => $message,
@ -1023,6 +1036,7 @@ try {
'enabled' => input_dir_enabled($dir),
'visible' => input_dir_visible($dir) && $canShow,
'can_show' => $canShow,
'can_permalink' => input_dir_can_permalink($dir, $config),
'dir' => basename($dir),
'id' => $articleId,
'message' => $message,
@ -1045,7 +1059,8 @@ try {
include_once __DIR__ . '/lib/send_push.php';
try_send_show_notification($articleId ?: basename($dir), $dir, __DIR__, '/var/log/mvlog_notify.log');
} else {
error_log("[mvlog] send_push helper missing, cannot send notification for " . basename($dir) . "\n", 3, '/var/log/mvlog_notify.log');
error_log("[mvlog] send_push helper missing, cannot send notification for " . basename($dir) . "
", 3, '/var/log/mvlog_notify.log');
}
}
@ -1056,6 +1071,27 @@ try {
'ok' => true,
'visible' => $currentVisible,
'can_show' => $canShow,
'can_permalink' => input_dir_can_permalink($dir, $config),
'dir' => basename($dir),
'id' => $articleId,
'message' => $message,
]);
}
header('Location: admin.php?tab=edit&msg=' . rawurlencode($message));
exit;
} elseif ($action === 'set_permalink') {
$dir = resolve_input_dir_from_request($config['uploads_dir'], $_POST['id'] ?? '', $_POST['dir'] ?? '', $articleIndex);
$articleId = ensure_state_article_id($dir);
$permalinkNow = !empty($_POST['permalink']);
$canPermalink = input_dir_can_permalink($dir, $config);
if ($permalinkNow && !$canPermalink) throw new RuntimeException('Only finished full-quality videos can use permalinks.');
set_input_dir_permalink($dir, $permalinkNow && $canPermalink);
$message = ($permalinkNow ? 'Permalink enabled for: in-dir/' : 'Permalink disabled for: in-dir/') . basename($dir);
if (!empty($_POST['ajax'])) {
ajax_json([
'ok' => true,
'permalink' => $permalinkNow && $canPermalink,
'can_permalink' => $canPermalink,
'dir' => basename($dir),
'id' => $articleId,
'message' => $message,
@ -1313,7 +1349,7 @@ $runningJobs = active_worker_jobs($config);
<?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==='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="admin.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 $map_n = ($displayOutput !== '' ? pathinfo($displayOutput, PATHINFO_FILENAME) : basename($d)) . '_map.png'; $hasMap = is_file($config['videos_dir'].'/'.$map_n); ?><?php if($hasVideo): ?><div class="video-wrapper"><video controls preload="metadata" src="<?=h($config['public_videos'].'/'.rawurlencode($displayOutput))?>"<?= $posterUrl !== '' ? ' poster="'.h($posterUrl).'"' : '' ?>></video><?php if($hasMap): ?><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 if($hasMap): ?><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; ?><?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">
<?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="admin.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); $permalink=input_dir_permalink($d); $canPermalink=input_dir_can_permalink($d, $config); $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))?>" data-permalink="<?= $permalink ? '1' : '0' ?>" data-can-permalink="<?= $canPermalink ? '1' : '0' ?>"><div><?php $map_n = ($displayOutput !== '' ? pathinfo($displayOutput, PATHINFO_FILENAME) : basename($d)) . '_map.png'; $hasMap = is_file($config['videos_dir'].'/'.$map_n); ?><?php if($hasVideo): ?><div class="video-wrapper"><video controls preload="metadata" src="<?=h($config['public_videos'].'/'.rawurlencode($displayOutput))?>"<?= $posterUrl !== '' ? ' poster="'.h($posterUrl).'"' : '' ?>></video><?php if($hasMap): ?><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 if($hasMap): ?><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; ?><?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">
<input type="hidden" name="action" value="set_preview">
<input type="hidden" name="dir" value="<?=h(basename($d))?>">
@ -1344,6 +1380,16 @@ $runningJobs = active_worker_jobs($config);
</label>
<noscript><button type="submit">Apply</button></noscript>
</form>
<form method="post" class="inline-form switch-form" data-switch="permalink">
<input type="hidden" name="action" value="set_permalink">
<input type="hidden" name="dir" value="<?=h(basename($d))?>">
<input type="hidden" name="id" value="<?=h(read_article_id($d))?>">
<label class="switch-label">
<input type="checkbox" name="permalink" value="1" <?=$permalink?'checked':''?> <?=!$canPermalink?'disabled':''?>>
<span class="switch-ui" aria-hidden="true"></span><span class="switch-text">Permalink</span>
</label>
<noscript><button type="submit">Apply</button></noscript>
</form>
<form method="post" class="inline-form switch-form" data-switch="hide-map">
<input type="hidden" name="action" value="set_map_hidden">
<input type="hidden" name="dir" value="<?=h(basename($d))?>">
@ -1355,7 +1401,7 @@ $runningJobs = active_worker_jobs($config);
<noscript><button type="submit">Apply</button></noscript>
</form>
<?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><?php $articleIdForLink = read_article_id($d); $titleText = h($meta['title'] ?: ($info['title'] ?? basename($d))); ?><h2><?php if(article_id_is_valid($articleIdForLink)): ?><a href="index.php?id=<?=rawurlencode($articleIdForLink)?>"><?= $titleText ?></a><?php else: ?><?= $titleText ?><?php endif; ?></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><?php $articleIdForLink = read_article_id($d); $titlePlain = $meta['title'] ?: ($info['title'] ?? basename($d)); $titleText = h($titlePlain); ?><h2 class="video-row-title" data-title-text="<?=h($titlePlain)?>" data-permalink-url="index.php?id=<?=rawurlencode($articleIdForLink)?>"><?php if(article_id_is_valid($articleIdForLink)): ?><a href="index.php?id=<?=rawurlencode($articleIdForLink)?>" target="_blank"><?= $titleText ?></a><?php else: ?><?= $titleText ?><?php endif; ?></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><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; ?><?php if(in_array($ext,['jpg','jpeg','png','webp','gif','mp4','mov','m4v','webm'])): ?><button type="button" class="button order-btn-up" title="Move up" onclick="moveFileOrder(<?=h(json_encode($f))?>,'up',this.closest('.caption-row'),this)">↑</button><button type="button" class="button order-btn-down" title="Move down" onclick="moveFileOrder(<?=h(json_encode($f))?>,'down',this.closest('.caption-row'),this)">↓</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 Article</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</button></form></section><?php endif; ?>
@ -1775,6 +1821,23 @@ async function handleSwitchToggle(form, checkbox){
}
}
function updateAdminRowTitleLink(row){
if(!row) return;
const titleEl = row.querySelector('h2');
if(!titleEl) return;
const title = titleEl.dataset.titleText || titleEl.textContent.trim();
const url = titleEl.dataset.permalinkUrl || '';
titleEl.textContent = '';
if(url){
const link = document.createElement('a');
link.href = url;
link.textContent = title;
titleEl.appendChild(link);
} else {
titleEl.textContent = title;
}
}
function applySwitchPayload(payload){
if(!payload) return;
let row = null;
@ -1790,6 +1853,10 @@ function applySwitchPayload(payload){
if('preview' in payload) setSwitch('preview', payload.preview);
if('enabled' in payload) setSwitch('render', payload.enabled);
if('map_hidden' in payload) setSwitch('hide-map', payload.map_hidden);
if('permalink' in payload){
setSwitch('permalink', payload.permalink);
row.dataset.permalink = payload.permalink ? '1' : '0';
}
if('visible' in payload){
setSwitch('show', payload.visible);
row.classList.toggle('shown', !!payload.visible);
@ -1805,6 +1872,14 @@ function applySwitchPayload(payload){
}
}
}
if('can_permalink' in payload){
row.dataset.canPermalink = payload.can_permalink ? '1' : '0';
const permalinkInput = row.querySelector('form[data-switch="permalink"] input[type="checkbox"]');
if(permalinkInput){
permalinkInput.disabled = !payload.can_permalink;
}
}
updateAdminRowTitleLink(row);
}
</script>
<script>

View file

@ -104,6 +104,27 @@ function video_article_id($config, $videoName){
}
return '';
}
function input_dirs($base){ return glob($base.'/*', GLOB_ONLYDIR) ?: []; }
function read_article_id($dir){
$idFile = rtrim((string)$dir, '/').'/.mvlog-id';
if (!is_file($idFile)) return '';
$id = trim((string)file_get_contents($idFile));
return preg_match('/^[0-9]{14}[a-f0-9]{16}$/', $id) ? $id : '';
}
function input_dir_permalink($dir){ return is_file($dir . '/.mvlog-permalink'); }
function article_permalink_enabled($config, $articleId){
static $cache = null;
if ($cache === null) {
$cache = [];
foreach (input_dirs($config['uploads_dir']) as $dir) {
$id = read_article_id($dir);
if ($id !== '') $cache[$id] = input_dir_permalink($dir);
}
}
$articleId = strtolower(trim((string)$articleId));
if (!preg_match('/^[0-9]{14}[a-f0-9]{16}$/', $articleId)) return false;
return !empty($cache[$articleId]);
}
function article_og_image_url($articleId){
$articleId = strtolower(trim((string)$articleId));
if (!preg_match('/^[0-9]{14}[a-f0-9]{16}$/', $articleId)) return '';
@ -252,14 +273,14 @@ function load_video_cache($videos){
$hiddenOutputs = hidden_video_outputs($config);
$hiddenMapOutputs = map_hidden_outputs($config);
$videos = array_values(array_filter(
$allVideos = array_values(array_filter(
glob($config['videos_dir'].'/*.{mp4,webm,mov,m4v}', GLOB_BRACE) ?: [],
fn($v)=>empty($hiddenOutputs[basename($v)]) && !preg_match('/_preview\.(mp4|webm|mov|m4v)$/i', basename($v))
fn($v)=>!preg_match('/_preview\.(mp4|webm|mov|m4v)$/i', basename($v))
));
$videoCache = load_video_cache($videos);
$videoCache = load_video_cache($allVideos);
usort($videos, function($a, $b) use ($videoCache) {
usort($allVideos, function($a, $b) use ($videoCache) {
$am = $videoCache[basename($a)] ?? [];
$bm = $videoCache[basename($b)] ?? [];
$ad = strtotime((string)($am['metadata']['sort_date'] ?? $am['metadata']['date'] ?? '')) ?: filemtime($a);
@ -271,18 +292,18 @@ $articleId = trim((string)($_GET['id'] ?? ''));
if ($articleId !== '' && (!preg_match('/^[0-9]{14}[a-f0-9]{16}$/', $articleId) || count(array_diff_key($_GET, ['id' => true])) > 0)) {
render_404_page();
}
if ($articleId === '') {
render_blank_page();
}
if ($articleId !== '') {
$videos = array_values(array_filter($videos, function($v) use ($articleId, $config) {
if (!article_permalink_enabled($config, $articleId)) {
render_404_page();
}
$videos = array_values(array_filter($allVideos, function($v) use ($articleId, $config) {
return video_article_id($config, basename($v)) === $articleId;
}));
$rawKeyword = '';
$keywordType = 'general';
$keywordValue = '';
} else {
$videos = array_values(array_filter($allVideos, fn($v) => empty($hiddenOutputs[basename($v)])));
$rawKeyword = trim((string)($_GET['q'] ?? ''));
// Backward compatibility with older filter/query formats.
@ -341,6 +362,9 @@ $pages = max(1, (int)ceil($total / $per));
$page = min($page, $pages);
$slice = array_slice($videos, ($page - 1) * $per, $per);
if (!$slice) {
if ($articleId === '') {
render_blank_page();
}
render_404_page();
}
@ -367,7 +391,6 @@ if ($articleId !== '') {
}
$headerTitles = require __DIR__ . '/header_titles.php';
$siteHeaderTitle = $headerTitles[array_rand($headerTitles)];
$brandUrl = '?id=' . rawurlencode($articleId);
?>
<!doctype html>
<html>
@ -409,8 +432,9 @@ $brandUrl = '?id=' . rawurlencode($articleId);
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
.filter-summary{color:#A0A4AB;font-size:.9rem;margin:0 0 .8rem}
.video-row-title{font-size:1.35rem;line-height:1.2;color:#F3F4F6}
.video-row-title a{color:inherit;text-decoration:none}
.video-row-title a:hover{text-decoration:underline}
.video-row-title a{color:inherit;text-decoration:none;cursor:pointer}
.video-row-title a:visited{color:inherit}
.video-row-title a:hover,.video-row-title a:focus,.video-row-title a:active{text-decoration:none}
.video-row-head-mobile{display:none}
.video-row-head-mobile .video-row-title{margin:0 0 .15rem}
.video-row-head-mobile .meta{margin:0 0 .25rem}
@ -469,7 +493,8 @@ $brandUrl = '?id=' . rawurlencode($articleId);
$name = basename($v);
$base = pathinfo($name, PATHINFO_FILENAME);
$articleIdForVideo = video_article_id($config, $name);
$permalinkUrl = $articleIdForVideo !== '' ? query_url(['id' => $articleIdForVideo]) : '';
$permalinkEnabled = $articleIdForVideo !== '' && article_permalink_enabled($config, $articleIdForVideo);
$permalinkUrl = $permalinkEnabled ? query_url(['id' => $articleIdForVideo]) : '';
$url = $config['public_videos'].'/'.rawurlencode($name);
$m = $videoCache[$name]['metadata'] ?? video_metadata($v);
$posterUrl = video_thumb_public_url($config, $name);

View file

@ -173,7 +173,7 @@ function try_send_show_notification($articleRefOrJob, $jobdir, $mvlog_root = nul
if ($articleTitle === '') $articleTitle = mvlog_nice_title((string)$job);
$articleUrl = './?id=' . rawurlencode($articleId);
$articleUrl = is_file($jobdir . '/.mvlog-permalink') ? './?id=' . rawurlencode($articleId) : './';
$payload = json_encode([
'title' => $articleTitle,

File diff suppressed because one or more lines are too long