Compare commits

...

3 commits

Author SHA1 Message Date
hbrain
f8c8fa2c4d Add describe prompt field to edit form 2026-06-15 05:26:42 +02:00
hbrain
d7a6184719 Add upper save button on edit form 2026-06-15 05:16:23 +02:00
hbrain
43e8476098 Allow permalink pages without showing on index 2026-06-15 04:46:26 +02:00
2 changed files with 36 additions and 5 deletions

View file

@ -365,6 +365,20 @@ function resolve_input_dir_from_request($base, $id, $dir, $articleIndex = null){
}
return resolve_input_dir($base, $dir, $articleIndex);
}
function read_describe_prompt($dir){
$file = rtrim((string)$dir, '/') . '/.mvlog-describe-prompt.txt';
return is_file($file) ? trim((string)@file_get_contents($file)) : '';
}
function write_describe_prompt($dir, $text){
$file = rtrim((string)$dir, '/') . '/.mvlog-describe-prompt.txt';
$text = trim((string)$text);
if ($text === '') {
if (is_file($file)) @unlink($file);
return;
}
file_put_contents($file, $text . PHP_EOL);
@chmod($file, 0664);
}
function read_data($dir){
$data = ['title'=>'','teaser'=>'','quote_da'=>'','date'=>'','place'=>'','description'=>'','captions'=>[],'video_audio'=>[]]; $file = $dir . '/data.txt';
if (!is_file($file)) return $data;
@ -476,6 +490,7 @@ function infer_date_from_first_media($dir){
return '';
}
function write_data($dir, $post){
if (array_key_exists('describe_prompt', $post)) write_describe_prompt($dir, $post['describe_prompt']);
$title = trim($post['title'] ?? '');
$teaser = trim($post['teaser'] ?? '');
$quote_da = trim($post['quote_da'] ?? '');
@ -1543,7 +1558,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; ?>
</div><div class="admin-actions"><?php if($running): ?><span class="button disabled" title="Rendering now" data-edit-action="1">Rendering</span><?php elseif($publicLocked): ?><a class="button disabled" title="Disable Show and Permalink before editing" aria-disabled="true" data-edit-action="1" data-edit-href="?id=<?=rawurlencode(read_article_id($d))?>&edit=<?=rawurlencode(basename($d))?>">Edit</a><?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); $dateFilterUrl = $meta['date'] ? ('admin.php?tab=edit&q=' . rawurlencode('date=' . $meta['date'])) : ''; $locFilterUrl = $meta['location'] ? ('admin.php?tab=edit&q=' . rawurlencode('location=' . $meta['location'])) : ''; ?><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']): ?><a class="meta-filter" href="<?=h($dateFilterUrl)?>" title="Filter by date"><?=h($meta['date'])?></a><?php endif; ?><?php if($meta['location']): ?><a class="meta-filter" href="<?=h($locFilterUrl)?>" title="Filter by location"><?=h($meta['location'])?></a><?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; ?><?php $detailParts = []; if(!$hasVideo) $detailParts[] = 'No video'; if($previewVideo) $detailParts[] = 'Preview video'; $detailParts[] = basename($d); $detailParts[] = (string)($info['file_count'] ?? count(list_files($d))) . ' files'; ?><p class="details"><?=h(implode(' · ', $detailParts))?></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==='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>Describe prompt<textarea name="describe_prompt" rows="4" placeholder="Optional extra guidance used by the Describe button"><?=h(read_describe_prompt($editDir))?></textarea></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><button type="submit">Save changes</button><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; ?>
<script>
function deleteFile(name){
@ -1901,7 +1916,6 @@ function handleAjaxFormSubmit(form, redirectTab) {
if(!res.ok || !json.ok) throw new Error(json.error || 'Operation failed');
success = true;
showToast(json.message || 'Operation successful', true);
if (useBusyOverlay) setTimeout(hideThumbBusy, 120);
setTimeout(() => { window.location.href = 'admin.php?tab=' + (json.tab || redirectTab); }, 450);
} catch(e) {
showToast(e.message || 'Operation failed', false);

View file

@ -84,6 +84,24 @@ function shown_video_paths($config){
return $videos;
}
function permalink_video_paths($config, $articleId){
$articleId = strtolower(trim((string)$articleId));
if (!preg_match('/^[0-9]{14}[a-f0-9]{16}$/', $articleId)) return [];
foreach (glob($config['uploads_dir'].'/*', GLOB_ONLYDIR) ?: [] as $dir) {
if (!is_file($dir . '/.mvlog-permalink')) continue;
if (read_article_id($dir) !== $articleId) continue;
$stateFile = $dir . '/.movmaker-state.json';
if (!is_file($stateFile)) return [];
$state = json_decode((string)file_get_contents($stateFile), true);
if (!is_array($state)) return [];
$name = basename((string)($state['output'] ?? ''));
if ($name === '' || preg_match('/_preview\.(mp4|webm|mov|m4v)$/i', $name)) return [];
$path = $config['videos_dir'] . '/' . $name;
return is_file($path) ? [$path] : [];
}
return [];
}
function public_url_path($path){
$path = str_replace('\\', '/', trim((string)$path));
if ($path === '') return '';
@ -309,9 +327,8 @@ if ($articleId !== '') {
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;
}));
// Permalink pages are allowed when Permalink is ON, even if Show is OFF.
$videos = permalink_video_paths($config, $articleId);
$rawKeyword = '';
$keywordType = 'general';
$keywordValue = '';