diff --git a/admin.php b/admin.php
index d23e5cf..889a98e 100644
--- a/admin.php
+++ b/admin.php
@@ -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);
=h($err)?>
Videos without input dir
No orphan videos.
=page_links('videos',$videoPage,$videoPages)?>
-No input directories yet.
(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' : ''); ?>
Thumb source: =h((string)$thumbMeta['source_file'])?>
+
No input directories yet.
(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' : ''); ?>
Thumb source: =h((string)$thumbMeta['source_file'])?>
+
-
=h($meta['date'])?>=h($meta['location'])?>No videoPreview video
=h((string)$meta['teaser'])?>
=nl2br(h($meta['description']), false)?>
โ=h(trim((string)$meta['quote_da'], "\"โโ"))?>โ
=h(basename($d))?> ยท =h($info['file_count'] ?? count(list_files($d)))?> files
=page_links('edit',$editPage,$editPages)?>
+
=h($meta['date'])?>=h($meta['location'])?>No videoPreview video
=h((string)$meta['teaser'])?>
=nl2br(h($meta['description']), false)?>
โ=h(trim((string)$meta['quote_da'], "\"โโ"))?>โ
=h(basename($d))?> ยท =h($info['file_count'] ?? count(list_files($d)))?> files
=page_links('edit',$editPage,$editPages)?>
Edit input dir
in-dir/=h(basename($editDir))?>
This input directory is =h($editStatus)?>. Editing is disabled until the job completes.
@@ -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);
}