Add persistent article IDs and ID-aware dir resolution in admin UI
This commit is contained in:
parent
76ffd9260c
commit
5475d3eece
1 changed files with 141 additions and 17 deletions
158
new.php
158
new.php
|
|
@ -4,6 +4,7 @@ mvlog_require_login();
|
||||||
$config = require __DIR__ . "/config.php";
|
$config = require __DIR__ . "/config.php";
|
||||||
foreach (["videos_dir", "thumbs_dir", "uploads_dir"] as $d) if (!is_dir($config[$d])) mkdir($config[$d], 0775, true);
|
foreach (["videos_dir", "thumbs_dir", "uploads_dir"] as $d) if (!is_dir($config[$d])) mkdir($config[$d], 0775, true);
|
||||||
if (!is_dir(__DIR__ . "/cache")) mkdir(__DIR__ . "/cache", 0775, true);
|
if (!is_dir(__DIR__ . "/cache")) mkdir(__DIR__ . "/cache", 0775, true);
|
||||||
|
const ARTICLE_ID_FILE = '.mvlog-id';
|
||||||
function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, "UTF-8"); }
|
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 ajax_json($data){ header('Content-Type: application/json; charset=UTF-8'); echo json_encode($data, JSON_UNESCAPED_UNICODE); exit; }
|
||||||
function ascii_safe($s){
|
function ascii_safe($s){
|
||||||
|
|
@ -22,6 +23,96 @@ function slugify($s){ $s = ascii_safe($s); $s = strtolower(trim($s)); $s = preg_
|
||||||
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 input_dirs($base){ return glob($base.'/*', GLOB_ONLYDIR) ?: []; }
|
function input_dirs($base){ return glob($base.'/*', GLOB_ONLYDIR) ?: []; }
|
||||||
function safe_input_dir($base, $name){ $name = basename((string)$name); $path = realpath($base . '/' . $name); $root = realpath($base); if (!$path || !$root || !str_starts_with($path, $root . DIRECTORY_SEPARATOR) || !is_dir($path)) throw new RuntimeException('Invalid input directory.'); return $path; }
|
function safe_input_dir($base, $name){ $name = basename((string)$name); $path = realpath($base . '/' . $name); $root = realpath($base); if (!$path || !$root || !str_starts_with($path, $root . DIRECTORY_SEPARATOR) || !is_dir($path)) throw new RuntimeException('Invalid input directory.'); return $path; }
|
||||||
|
function article_id_is_valid($id){ return is_string($id) && preg_match('/^[0-9]{14}[a-f0-9]{16}$/', $id); }
|
||||||
|
function generate_article_id(){ return gmdate('YmdHis') . bin2hex(random_bytes(8)); }
|
||||||
|
function read_article_id($dir){
|
||||||
|
$idFile = rtrim((string)$dir, '/').'/'.ARTICLE_ID_FILE;
|
||||||
|
if (!is_file($idFile)) return '';
|
||||||
|
$id = trim((string)file_get_contents($idFile));
|
||||||
|
return article_id_is_valid($id) ? $id : '';
|
||||||
|
}
|
||||||
|
function write_article_id($dir, $id){
|
||||||
|
if (!article_id_is_valid($id)) throw new RuntimeException('Invalid article id.');
|
||||||
|
$idFile = rtrim((string)$dir, '/').'/'.ARTICLE_ID_FILE;
|
||||||
|
$tmp = $idFile . '.tmp.' . bin2hex(random_bytes(4));
|
||||||
|
if (file_put_contents($tmp, $id . "\n", LOCK_EX) === false) throw new RuntimeException('Cannot write article id.');
|
||||||
|
chmod($tmp, 0664);
|
||||||
|
if (!rename($tmp, $idFile)) { @unlink($tmp); throw new RuntimeException('Cannot save article id.'); }
|
||||||
|
}
|
||||||
|
function ensure_article_id($dir, &$seenIds = null){
|
||||||
|
$existing = read_article_id($dir);
|
||||||
|
if ($existing !== '' && (!is_array($seenIds) || empty($seenIds[$existing]))) {
|
||||||
|
if (is_array($seenIds)) $seenIds[$existing] = basename((string)$dir);
|
||||||
|
return $existing;
|
||||||
|
}
|
||||||
|
do { $id = generate_article_id(); } while (is_array($seenIds) && !empty($seenIds[$id]));
|
||||||
|
write_article_id($dir, $id);
|
||||||
|
if (is_array($seenIds)) $seenIds[$id] = basename((string)$dir);
|
||||||
|
return $id;
|
||||||
|
}
|
||||||
|
function article_index_signature($dirs){
|
||||||
|
$names = array_map('basename', $dirs);
|
||||||
|
sort($names, SORT_STRING);
|
||||||
|
$parts = [];
|
||||||
|
foreach ($names as $name) $parts[] = $name;
|
||||||
|
return hash('sha256', implode("\n", $parts));
|
||||||
|
}
|
||||||
|
function load_articles_index($base, $dirs = null){
|
||||||
|
if ($dirs === null) $dirs = input_dirs($base);
|
||||||
|
$cacheFile = __DIR__ . '/cache/articles-index.json';
|
||||||
|
$signature = article_index_signature($dirs);
|
||||||
|
$cache = is_file($cacheFile) ? json_decode((string)file_get_contents($cacheFile), true) : [];
|
||||||
|
if (is_array($cache)
|
||||||
|
&& ($cache['version'] ?? 0) === 1
|
||||||
|
&& ($cache['signature'] ?? '') === $signature
|
||||||
|
&& is_array($cache['by_id'] ?? null)
|
||||||
|
&& is_array($cache['by_dir'] ?? null)
|
||||||
|
&& !in_array('', $cache['by_dir'], true)) {
|
||||||
|
return $cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
$byId = [];
|
||||||
|
$byDir = [];
|
||||||
|
foreach ($dirs as $dir) {
|
||||||
|
$name = basename((string)$dir);
|
||||||
|
$id = read_article_id($dir);
|
||||||
|
if (($id === '' || !empty($byId[$id])) && !is_dir($dir . '/.movmaker-lock')) {
|
||||||
|
$id = ensure_article_id($dir, $byId);
|
||||||
|
}
|
||||||
|
if ($id !== '' && empty($byId[$id])) $byId[$id] = $name;
|
||||||
|
$byDir[$name] = $id;
|
||||||
|
}
|
||||||
|
ksort($byId, SORT_STRING);
|
||||||
|
ksort($byDir, SORT_STRING);
|
||||||
|
$out = [
|
||||||
|
'version' => 1,
|
||||||
|
'generated_at' => date('c'),
|
||||||
|
'signature' => $signature,
|
||||||
|
'by_id' => $byId,
|
||||||
|
'by_dir' => $byDir,
|
||||||
|
];
|
||||||
|
file_put_contents($cacheFile, json_encode($out, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), LOCK_EX);
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
function resolve_input_dir($base, $idOrDir, $articleIndex = null){
|
||||||
|
$token = trim((string)$idOrDir);
|
||||||
|
if ($token === '') throw new RuntimeException('Invalid input directory.');
|
||||||
|
if ($articleIndex === null) $articleIndex = load_articles_index($base);
|
||||||
|
if (article_id_is_valid($token)) {
|
||||||
|
$name = (string)($articleIndex['by_id'][$token] ?? '');
|
||||||
|
if ($name !== '') return safe_input_dir($base, $name);
|
||||||
|
}
|
||||||
|
return safe_input_dir($base, $token);
|
||||||
|
}
|
||||||
|
function resolve_input_dir_from_request($base, $id, $dir, $articleIndex = null){
|
||||||
|
$id = trim((string)$id);
|
||||||
|
$dir = trim((string)$dir);
|
||||||
|
if ($id !== '') {
|
||||||
|
try { return resolve_input_dir($base, $id, $articleIndex); }
|
||||||
|
catch (Throwable $e) { if ($dir !== '') return resolve_input_dir($base, $dir, $articleIndex); throw $e; }
|
||||||
|
}
|
||||||
|
return resolve_input_dir($base, $dir, $articleIndex);
|
||||||
|
}
|
||||||
function read_data($dir){
|
function read_data($dir){
|
||||||
$data = ['title'=>'','date'=>'','place'=>'','description'=>'','captions'=>[],'video_audio'=>[]]; $file = $dir . '/data.txt';
|
$data = ['title'=>'','date'=>'','place'=>'','description'=>'','captions'=>[],'video_audio'=>[]]; $file = $dir . '/data.txt';
|
||||||
if (!is_file($file)) return $data;
|
if (!is_file($file)) return $data;
|
||||||
|
|
@ -281,7 +372,7 @@ entries = []
|
||||||
for dirpath, dirnames, filenames in os.walk(root):
|
for dirpath, dirnames, filenames in os.walk(root):
|
||||||
dirnames[:] = [d for d in dirnames if d != '.movmaker-lock']
|
dirnames[:] = [d for d in dirnames if d != '.movmaker-lock']
|
||||||
for name in filenames:
|
for name in filenames:
|
||||||
if name in {'.movmaker-state.json', '.movmaker-enabled', '.movmaker-preview', '.mvlog-hidden'}:
|
if name in {'.movmaker-state.json', '.movmaker-enabled', '.movmaker-preview', '.mvlog-hidden', '.mvlog-id'}:
|
||||||
continue
|
continue
|
||||||
path = os.path.join(dirpath, name)
|
path = os.path.join(dirpath, name)
|
||||||
rel = os.path.relpath(path, root)
|
rel = os.path.relpath(path, root)
|
||||||
|
|
@ -300,6 +391,7 @@ function input_dir_info($dir){
|
||||||
$files = list_files($dir);
|
$files = list_files($dir);
|
||||||
return [
|
return [
|
||||||
'name' => basename($dir),
|
'name' => basename($dir),
|
||||||
|
'article_id' => ensure_article_id($dir),
|
||||||
'title' => $data['title'] ?: basename($dir),
|
'title' => $data['title'] ?: basename($dir),
|
||||||
'sort_date' => input_dir_date_from_text($data['date'] ?? '') ?? input_dir_date_from_media($dir),
|
'sort_date' => input_dir_date_from_text($data['date'] ?? '') ?? input_dir_date_from_media($dir),
|
||||||
'file_count' => count($files),
|
'file_count' => count($files),
|
||||||
|
|
@ -316,7 +408,7 @@ function load_input_dir_cache($dirs){
|
||||||
$name = basename($dir);
|
$name = basename($dir);
|
||||||
$sig = input_dir_signature($dir);
|
$sig = input_dir_signature($dir);
|
||||||
$cached = $oldItems[$name] ?? null;
|
$cached = $oldItems[$name] ?? null;
|
||||||
if (is_array($cached) && ($cached['signature'] ?? '') === $sig) {
|
if (is_array($cached) && ($cached['signature'] ?? '') === $sig && !empty($cached['article_id'])) {
|
||||||
$items[$name] = $cached;
|
$items[$name] = $cached;
|
||||||
} else {
|
} else {
|
||||||
$items[$name] = input_dir_info($dir);
|
$items[$name] = input_dir_info($dir);
|
||||||
|
|
@ -380,10 +472,10 @@ function set_input_dir_preview($dir, $preview){
|
||||||
$stateFile = $dir . '/.movmaker-state.json';
|
$stateFile = $dir . '/.movmaker-state.json';
|
||||||
$state = is_file($stateFile) ? json_decode((string)file_get_contents($stateFile), true) : [];
|
$state = is_file($stateFile) ? json_decode((string)file_get_contents($stateFile), true) : [];
|
||||||
if (!is_array($state)) $state = [];
|
if (!is_array($state)) $state = [];
|
||||||
|
$state['article_id'] = ensure_article_id($dir);
|
||||||
$state['preview'] = (bool)$preview;
|
$state['preview'] = (bool)$preview;
|
||||||
$state['preview_updated_at'] = gmdate('c');
|
$state['preview_updated_at'] = gmdate('c');
|
||||||
file_put_contents($stateFile, json_encode($state, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "
|
file_put_contents($stateFile, json_encode($state, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n");
|
||||||
");
|
|
||||||
chmod($stateFile, 0664);
|
chmod($stateFile, 0664);
|
||||||
}
|
}
|
||||||
function input_dir_visible($dir){ return !is_file($dir . '/.mvlog-hidden'); }
|
function input_dir_visible($dir){ return !is_file($dir . '/.mvlog-hidden'); }
|
||||||
|
|
@ -392,10 +484,24 @@ function set_input_dir_visible($dir, $visible){
|
||||||
if ($visible) { if (is_file($path)) unlink($path); }
|
if ($visible) { if (is_file($path)) unlink($path); }
|
||||||
else { file_put_contents($path, "hidden\n"); chmod($path, 0664); }
|
else { file_put_contents($path, "hidden\n"); chmod($path, 0664); }
|
||||||
}
|
}
|
||||||
|
function ensure_state_article_id($dir){
|
||||||
|
$articleId = ensure_article_id($dir);
|
||||||
|
$stateFile = $dir . '/.movmaker-state.json';
|
||||||
|
$state = is_file($stateFile) ? json_decode((string)file_get_contents($stateFile), true) : [];
|
||||||
|
if (!is_array($state)) $state = [];
|
||||||
|
if (($state['article_id'] ?? '') !== $articleId) {
|
||||||
|
$state['article_id'] = $articleId;
|
||||||
|
file_put_contents($stateFile, json_encode($state, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n");
|
||||||
|
chmod($stateFile, 0664);
|
||||||
|
}
|
||||||
|
return $articleId;
|
||||||
|
}
|
||||||
function input_dir_state($dir){
|
function input_dir_state($dir){
|
||||||
$stateFile = $dir . '/.movmaker-state.json';
|
$stateFile = $dir . '/.movmaker-state.json';
|
||||||
$state = is_file($stateFile) ? json_decode((string)file_get_contents($stateFile), true) : [];
|
$state = is_file($stateFile) ? json_decode((string)file_get_contents($stateFile), true) : [];
|
||||||
return is_array($state) ? $state : [];
|
if (!is_array($state)) $state = [];
|
||||||
|
$state['article_id'] = ($state['article_id'] ?? '') ?: read_article_id($dir);
|
||||||
|
return $state;
|
||||||
}
|
}
|
||||||
function active_worker_jobs($config){
|
function active_worker_jobs($config){
|
||||||
$jobs = [];
|
$jobs = [];
|
||||||
|
|
@ -507,6 +613,7 @@ function save_uploads($field, $dest, $allowed){
|
||||||
$msg = $_GET['msg'] ?? null; $err = null;
|
$msg = $_GET['msg'] ?? null; $err = null;
|
||||||
$allowedMedia = ['jpg','jpeg','png','webp','gif','mp4','mov','m4v','avi','mkv','webm'];
|
$allowedMedia = ['jpg','jpeg','png','webp','gif','mp4','mov','m4v','avi','mkv','webm'];
|
||||||
$allowedAudio = ['mp3','wav','m4a','aac','ogg','flac'];
|
$allowedAudio = ['mp3','wav','m4a','aac','ogg','flac'];
|
||||||
|
$articleIndex = load_articles_index($config['uploads_dir']);
|
||||||
try {
|
try {
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
$action = $_POST['action'] ?? 'create';
|
$action = $_POST['action'] ?? 'create';
|
||||||
|
|
@ -526,17 +633,19 @@ try {
|
||||||
$slug = basename($dir);
|
$slug = basename($dir);
|
||||||
if (!rename($tmpDir, $dir)) { rrmdir($tmpDir); throw new RuntimeException('Cannot create input directory.'); }
|
if (!rename($tmpDir, $dir)) { rrmdir($tmpDir); throw new RuntimeException('Cannot create input directory.'); }
|
||||||
chmod($dir, 02775);
|
chmod($dir, 02775);
|
||||||
|
$articleId = ensure_article_id($dir);
|
||||||
write_data($dir, $_POST);
|
write_data($dir, $_POST);
|
||||||
set_input_dir_enabled($dir, false);
|
set_input_dir_enabled($dir, false);
|
||||||
set_input_dir_preview($dir, false);
|
set_input_dir_preview($dir, false);
|
||||||
set_input_dir_visible($dir, false);
|
set_input_dir_visible($dir, false);
|
||||||
if (!empty($_POST['ajax'])) {
|
if (!empty($_POST['ajax'])) {
|
||||||
ajax_json(['ok'=>true, 'message'=>"Created movmaker input directory: in-dir/$slug", 'dir'=>$slug, 'tab'=>'edit']);
|
ajax_json(['ok'=>true, 'message'=>"Created movmaker input directory: in-dir/$slug", 'dir'=>$slug, 'id'=>$articleId, 'tab'=>'edit']);
|
||||||
}
|
}
|
||||||
header('Location: new.php?tab=edit');
|
header('Location: new.php?tab=edit');
|
||||||
exit;
|
exit;
|
||||||
} elseif ($action === 'set_enabled') {
|
} elseif ($action === 'set_enabled') {
|
||||||
$dir = safe_input_dir($config['uploads_dir'], $_POST['dir'] ?? '');
|
$dir = resolve_input_dir_from_request($config['uploads_dir'], $_POST['id'] ?? '', $_POST['dir'] ?? '', $articleIndex);
|
||||||
|
$articleId = ensure_state_article_id($dir);
|
||||||
$enabledNow = !empty($_POST['enabled']);
|
$enabledNow = !empty($_POST['enabled']);
|
||||||
set_input_dir_enabled($dir, $enabledNow);
|
set_input_dir_enabled($dir, $enabledNow);
|
||||||
if ($enabledNow) {
|
if ($enabledNow) {
|
||||||
|
|
@ -555,13 +664,15 @@ try {
|
||||||
'visible' => input_dir_visible($dir) && $canShow,
|
'visible' => input_dir_visible($dir) && $canShow,
|
||||||
'can_show' => $canShow,
|
'can_show' => $canShow,
|
||||||
'dir' => basename($dir),
|
'dir' => basename($dir),
|
||||||
|
'id' => $articleId,
|
||||||
'message' => $message,
|
'message' => $message,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
header('Location: new.php?tab=edit&msg=' . rawurlencode($message));
|
header('Location: new.php?tab=edit&msg=' . rawurlencode($message));
|
||||||
exit;
|
exit;
|
||||||
} elseif ($action === 'set_preview') {
|
} elseif ($action === 'set_preview') {
|
||||||
$dir = safe_input_dir($config['uploads_dir'], $_POST['dir'] ?? '');
|
$dir = resolve_input_dir_from_request($config['uploads_dir'], $_POST['id'] ?? '', $_POST['dir'] ?? '', $articleIndex);
|
||||||
|
$articleId = ensure_state_article_id($dir);
|
||||||
$previewNow = !empty($_POST['preview']);
|
$previewNow = !empty($_POST['preview']);
|
||||||
set_input_dir_preview($dir, $previewNow);
|
set_input_dir_preview($dir, $previewNow);
|
||||||
if ($previewNow) { set_input_dir_enabled($dir, false); set_input_dir_visible($dir, false); }
|
if ($previewNow) { set_input_dir_enabled($dir, false); set_input_dir_visible($dir, false); }
|
||||||
|
|
@ -577,13 +688,15 @@ try {
|
||||||
'visible' => input_dir_visible($dir) && $canShow,
|
'visible' => input_dir_visible($dir) && $canShow,
|
||||||
'can_show' => $canShow,
|
'can_show' => $canShow,
|
||||||
'dir' => basename($dir),
|
'dir' => basename($dir),
|
||||||
|
'id' => $articleId,
|
||||||
'message' => $message,
|
'message' => $message,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
header('Location: new.php?tab=edit&msg=' . rawurlencode($message));
|
header('Location: new.php?tab=edit&msg=' . rawurlencode($message));
|
||||||
exit;
|
exit;
|
||||||
} elseif ($action === 'set_visible') {
|
} elseif ($action === 'set_visible') {
|
||||||
$dir = safe_input_dir($config['uploads_dir'], $_POST['dir'] ?? '');
|
$dir = resolve_input_dir_from_request($config['uploads_dir'], $_POST['id'] ?? '', $_POST['dir'] ?? '', $articleIndex);
|
||||||
|
$articleId = ensure_state_article_id($dir);
|
||||||
$visibleNow = !empty($_POST['visible']);
|
$visibleNow = !empty($_POST['visible']);
|
||||||
$state = input_dir_state($dir);
|
$state = input_dir_state($dir);
|
||||||
$output = basename((string)($state['output'] ?? ''));
|
$output = basename((string)($state['output'] ?? ''));
|
||||||
|
|
@ -608,24 +721,27 @@ try {
|
||||||
'visible' => $currentVisible,
|
'visible' => $currentVisible,
|
||||||
'can_show' => $canShow,
|
'can_show' => $canShow,
|
||||||
'dir' => basename($dir),
|
'dir' => basename($dir),
|
||||||
|
'id' => $articleId,
|
||||||
'message' => $message,
|
'message' => $message,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
header('Location: new.php?tab=edit&msg=' . rawurlencode($message));
|
header('Location: new.php?tab=edit&msg=' . rawurlencode($message));
|
||||||
exit;
|
exit;
|
||||||
} elseif ($action === 'update') {
|
} elseif ($action === 'update') {
|
||||||
$dir = safe_input_dir($config['uploads_dir'], $_POST['dir'] ?? '');
|
$dir = resolve_input_dir_from_request($config['uploads_dir'], $_POST['id'] ?? '', $_POST['dir'] ?? '', $articleIndex);
|
||||||
|
$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);
|
write_data($dir, $_POST);
|
||||||
save_uploads('media', $dir, $allowedMedia);
|
save_uploads('media', $dir, $allowedMedia);
|
||||||
save_uploads('audio', $dir, $allowedAudio);
|
save_uploads('audio', $dir, $allowedAudio);
|
||||||
// Always regenerate map image(s) on Save changes.
|
// Always regenerate map image(s) on Save changes.
|
||||||
regenerate_input_dir_maps($dir, $config);
|
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)]);
|
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;
|
||||||
} elseif ($action === 'delete_file') {
|
} elseif ($action === 'delete_file') {
|
||||||
$dir = safe_input_dir($config['uploads_dir'], $_POST['dir'] ?? '');
|
$dir = resolve_input_dir_from_request($config['uploads_dir'], $_POST['id'] ?? '', $_POST['dir'] ?? '', $articleIndex);
|
||||||
|
$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.');
|
||||||
$file = basename((string)($_POST['file'] ?? ''));
|
$file = basename((string)($_POST['file'] ?? ''));
|
||||||
if (!editable_file($file) || !is_file($dir.'/'.$file)) throw new RuntimeException('Invalid file.');
|
if (!editable_file($file) || !is_file($dir.'/'.$file)) throw new RuntimeException('Invalid file.');
|
||||||
|
|
@ -648,7 +764,8 @@ try {
|
||||||
header('Location: new.php?tab=videos');
|
header('Location: new.php?tab=videos');
|
||||||
exit;
|
exit;
|
||||||
} elseif ($action === 'delete_dir') {
|
} elseif ($action === 'delete_dir') {
|
||||||
$dir = safe_input_dir($config['uploads_dir'], $_POST['dir'] ?? '');
|
$dir = resolve_input_dir_from_request($config['uploads_dir'], $_POST['id'] ?? '', $_POST['dir'] ?? '', $articleIndex);
|
||||||
|
$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.');
|
||||||
$name = basename($dir);
|
$name = basename($dir);
|
||||||
$stateFile = $dir . '/.movmaker-state.json';
|
$stateFile = $dir . '/.movmaker-state.json';
|
||||||
|
|
@ -676,6 +793,7 @@ try {
|
||||||
$err = $e->getMessage();
|
$err = $e->getMessage();
|
||||||
}
|
}
|
||||||
$dirs = input_dirs($config['uploads_dir']);
|
$dirs = input_dirs($config['uploads_dir']);
|
||||||
|
$articleIndex = load_articles_index($config['uploads_dir'], $dirs);
|
||||||
$dirInfo = load_input_dir_cache($dirs);
|
$dirInfo = load_input_dir_cache($dirs);
|
||||||
sort_input_dirs_by_metadata_date($dirs, $dirInfo);
|
sort_input_dirs_by_metadata_date($dirs, $dirInfo);
|
||||||
$orphanVideos = orphan_videos($config);
|
$orphanVideos = orphan_videos($config);
|
||||||
|
|
@ -716,8 +834,9 @@ $page = max(1, (int)($_GET['page'] ?? 1));
|
||||||
[$editPage, $editPages, $dirsPage, $dirsTotal] = paginate($dirs, $tab === 'edit' ? $page : 1);
|
[$editPage, $editPages, $dirsPage, $dirsTotal] = paginate($dirs, $tab === 'edit' ? $page : 1);
|
||||||
[$videoPage, $videoPages, $orphanVideosPage, $orphanVideosTotal] = paginate($orphanVideos, $tab === 'videos' ? $page : 1);
|
[$videoPage, $videoPages, $orphanVideosPage, $orphanVideosTotal] = paginate($orphanVideos, $tab === 'videos' ? $page : 1);
|
||||||
$editName = $_GET['edit'] ?? '';
|
$editName = $_GET['edit'] ?? '';
|
||||||
|
$editId = $_GET['id'] ?? '';
|
||||||
$editDir = null; $editStatus = ''; $editRunning = false; $editData = ['title'=>'','date'=>'','place'=>'','description'=>'','captions'=>[],'video_audio'=>[]]; $editFiles = [];
|
$editDir = null; $editStatus = ''; $editRunning = false; $editData = ['title'=>'','date'=>'','place'=>'','description'=>'','captions'=>[],'video_audio'=>[]]; $editFiles = [];
|
||||||
if ($editName !== '') { try { $editDir = safe_input_dir($config['uploads_dir'], $editName); $editStatus = input_dir_status($editDir); $editRunning = input_dir_running($editDir); if (!$editRunning) { $editData = read_data($editDir); $editFiles = media_sort_files($editDir, list_files($editDir)); } } catch (Throwable $e) { $err = $e->getMessage(); } }
|
if ($editName !== '' || $editId !== '') { try { $editDir = resolve_input_dir_from_request($config['uploads_dir'], $editId, $editName, $articleIndex); $editStatus = input_dir_status($editDir); $editRunning = input_dir_running($editDir); if (!$editRunning) { $editData = read_data($editDir); $editFiles = media_sort_files($editDir, list_files($editDir)); } } catch (Throwable $e) { $err = $e->getMessage(); } }
|
||||||
$runningJobs = active_worker_jobs($config);
|
$runningJobs = active_worker_jobs($config);
|
||||||
?>
|
?>
|
||||||
<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Admin - <?=h($config['site_name'])?></title><link rel="icon" type="image/png" href="assets/img/moto_travel.png"><link rel="stylesheet" href="style.css"><style>.admin-search-form{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:.55rem;align-items:center;margin:0 0 .6rem}.admin-search-wrap{position:relative;min-width:0}.admin-search-wrap input{margin:0;padding-right:2.15rem}.admin-search-clear{position:absolute;right:.45rem;top:50%;transform:translateY(-50%);display:inline-flex;align-items:center;justify-content:center;width:1.5rem;height:1.5rem;border-radius:50%;background:#C46A3A;color:#111315;text-decoration:none;font-size:1rem;line-height:1;font-weight:700;box-shadow:0 1px 4px #0007;transition:opacity .15s ease,transform .15s ease}.admin-search-clear:hover{background:#d97b48;color:#111315;transform:translateY(-50%) scale(1.05)}.admin-search-clear.is-empty{opacity:.38;pointer-events:none}.admin-search-submit{display:inline-grid;place-items:center;width:2.35rem;height:2.35rem;padding:0;border-radius:.5rem;background:#C46A3A;color:#111315}.admin-search-submit .icon{font-size:1.02rem;line-height:1;transform:translateY(.01em)}.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}</style></head><body>
|
<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Admin - <?=h($config['site_name'])?></title><link rel="icon" type="image/png" href="assets/img/moto_travel.png"><link rel="stylesheet" href="style.css"><style>.admin-search-form{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:.55rem;align-items:center;margin:0 0 .6rem}.admin-search-wrap{position:relative;min-width:0}.admin-search-wrap input{margin:0;padding-right:2.15rem}.admin-search-clear{position:absolute;right:.45rem;top:50%;transform:translateY(-50%);display:inline-flex;align-items:center;justify-content:center;width:1.5rem;height:1.5rem;border-radius:50%;background:#C46A3A;color:#111315;text-decoration:none;font-size:1rem;line-height:1;font-weight:700;box-shadow:0 1px 4px #0007;transition:opacity .15s ease,transform .15s ease}.admin-search-clear:hover{background:#d97b48;color:#111315;transform:translateY(-50%) scale(1.05)}.admin-search-clear.is-empty{opacity:.38;pointer-events:none}.admin-search-submit{display:inline-grid;place-items:center;width:2.35rem;height:2.35rem;padding:0;border-radius:.5rem;background:#C46A3A;color:#111315}.admin-search-submit .icon{font-size:1.02rem;line-height:1;transform:translateY(.01em)}.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}</style></head><body>
|
||||||
|
|
@ -730,6 +849,7 @@ $runningJobs = active_worker_jobs($config);
|
||||||
<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))?>">
|
||||||
|
<input type="hidden" name="id" value="<?=h(read_article_id($d))?>">
|
||||||
<label class="switch-label">
|
<label class="switch-label">
|
||||||
<input type="checkbox" name="preview" value="1" <?=$preview?'checked':''?>>
|
<input type="checkbox" name="preview" value="1" <?=$preview?'checked':''?>>
|
||||||
<span class="switch-ui" aria-hidden="true"></span><span class="switch-text">Preview</span>
|
<span class="switch-ui" aria-hidden="true"></span><span class="switch-text">Preview</span>
|
||||||
|
|
@ -739,6 +859,7 @@ $runningJobs = active_worker_jobs($config);
|
||||||
<form method="post" class="inline-form switch-form" data-switch="render">
|
<form method="post" class="inline-form switch-form" data-switch="render">
|
||||||
<input type="hidden" name="action" value="set_enabled">
|
<input type="hidden" name="action" value="set_enabled">
|
||||||
<input type="hidden" name="dir" value="<?=h(basename($d))?>">
|
<input type="hidden" name="dir" value="<?=h(basename($d))?>">
|
||||||
|
<input type="hidden" name="id" value="<?=h(read_article_id($d))?>">
|
||||||
<label class="switch-label">
|
<label class="switch-label">
|
||||||
<input type="checkbox" name="enabled" value="1" <?=$enabled?'checked':''?>>
|
<input type="checkbox" name="enabled" value="1" <?=$enabled?'checked':''?>>
|
||||||
<span class="switch-ui" aria-hidden="true"></span><span class="switch-text">Render</span>
|
<span class="switch-ui" aria-hidden="true"></span><span class="switch-text">Render</span>
|
||||||
|
|
@ -748,6 +869,7 @@ $runningJobs = active_worker_jobs($config);
|
||||||
<form method="post" class="inline-form switch-form" data-switch="show">
|
<form method="post" class="inline-form switch-form" data-switch="show">
|
||||||
<input type="hidden" name="action" value="set_visible">
|
<input type="hidden" name="action" value="set_visible">
|
||||||
<input type="hidden" name="dir" value="<?=h(basename($d))?>">
|
<input type="hidden" name="dir" value="<?=h(basename($d))?>">
|
||||||
|
<input type="hidden" name="id" value="<?=h(read_article_id($d))?>">
|
||||||
<label class="switch-label">
|
<label class="switch-label">
|
||||||
<input type="checkbox" name="visible" value="1" <?=$visible?'checked':''?> <?=!$canShow?'disabled':''?>>
|
<input type="checkbox" name="visible" value="1" <?=$visible?'checked':''?> <?=!$canShow?'disabled':''?>>
|
||||||
<span class="switch-ui" aria-hidden="true"></span><span class="switch-text">Show</span>
|
<span class="switch-ui" aria-hidden="true"></span><span class="switch-text">Show</span>
|
||||||
|
|
@ -755,9 +877,9 @@ $runningJobs = active_worker_jobs($config);
|
||||||
<noscript><button type="submit">Apply</button></noscript>
|
<noscript><button type="submit">Apply</button></noscript>
|
||||||
</form>
|
</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; ?>
|
<?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="?edit=<?=rawurlencode(basename($d))?>" data-edit-action="1" data-edit-href="?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($meta['description']): ?><p class="description"><?=nl2br(h($meta['description']), false)?></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($meta['description']): ?><p class="description"><?=nl2br(h($meta['description']), false)?></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))?>"><label>Title<input name="title" value="<?=h($editData['title'])?>"></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><button type="button" onclick="deleteFile(<?=h(json_encode($f))?>)">Remove</button></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="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))?>"><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>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><button type="button" onclick="deleteFile(<?=h(json_encode($f))?>)">Remove</button></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>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>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){
|
||||||
|
|
@ -766,6 +888,8 @@ function deleteFile(name){
|
||||||
if(!form){ showToast('Internal error: delete form not found', false); return; }
|
if(!form){ showToast('Internal error: delete form not found', false); return; }
|
||||||
var dirInput = form.querySelector('input[name=dir]');
|
var dirInput = form.querySelector('input[name=dir]');
|
||||||
var dir = dirInput ? dirInput.value : '';
|
var dir = dirInput ? dirInput.value : '';
|
||||||
|
var idInput = form.querySelector('input[name=id]');
|
||||||
|
var articleId = idInput ? idInput.value : '';
|
||||||
|
|
||||||
// find the caption-row for this file name
|
// find the caption-row for this file name
|
||||||
var rows = document.querySelectorAll('.caption-row');
|
var rows = document.querySelectorAll('.caption-row');
|
||||||
|
|
@ -776,7 +900,7 @@ function deleteFile(name){
|
||||||
if(strong.textContent.trim() === name){ target = rows[i]; break; }
|
if(strong.textContent.trim() === name){ target = rows[i]; break; }
|
||||||
}
|
}
|
||||||
|
|
||||||
var body = 'action=delete_file&dir=' + encodeURIComponent(dir) + '&file=' + encodeURIComponent(name) + '&ajax=1';
|
var body = 'action=delete_file&dir=' + encodeURIComponent(dir) + '&id=' + encodeURIComponent(articleId) + '&file=' + encodeURIComponent(name) + '&ajax=1';
|
||||||
fetch('new.php', {
|
fetch('new.php', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue