Add admin AJAX up/down media reordering synced to order.json

This commit is contained in:
hbrain 2026-05-31 17:33:19 +02:00
parent 7296c5cef4
commit 3cc2828bc0

195
new.php
View file

@ -339,6 +339,51 @@ function list_files($dir){
function media_sort_files($dir, $files){ function media_sort_files($dir, $files){
return list_files($dir); return list_files($dir);
} }
function visual_order_files($dir){
$all = list_files($dir);
return array_values(array_filter($all, fn($name)=>is_visual_media_file($name)));
}
function write_visual_order($dir, $order){
$clean = [];
$seen = [];
foreach ((array)$order as $name) {
if (!is_string($name)) continue;
$name = basename($name);
if (!is_visual_media_file($name) || isset($seen[$name])) continue;
if (!is_file($dir . '/' . $name)) continue;
$seen[$name] = true;
$clean[] = $name;
}
file_put_contents($dir . '/order.json', json_encode(array_values($clean), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
@chmod($dir . '/order.json', 0664);
return $clean;
}
function move_visual_file_order($dir, $file, $direction){
$file = basename((string)$file);
if (!is_visual_media_file($file) || !is_file($dir . '/' . $file)) throw new RuntimeException('Invalid media file for reordering.');
$direction = strtolower(trim((string)$direction));
if (!in_array($direction, ['up','down'], true)) throw new RuntimeException('Invalid move direction.');
$order = visual_order_files($dir);
$idx = array_search($file, $order, true);
if ($idx === false) throw new RuntimeException('File not found in visual order.');
$newIdx = $idx;
if ($direction === 'up' && $idx > 0) {
[$order[$idx - 1], $order[$idx]] = [$order[$idx], $order[$idx - 1]];
$newIdx = $idx - 1;
} elseif ($direction === 'down' && $idx < count($order) - 1) {
[$order[$idx], $order[$idx + 1]] = [$order[$idx + 1], $order[$idx]];
$newIdx = $idx + 1;
}
write_visual_order($dir, $order);
return [
'changed' => $newIdx !== $idx,
'index' => $newIdx,
'count' => count($order),
];
}
function input_dir_date_from_media($dir){ function input_dir_date_from_media($dir){
$dates = []; $dates = [];
foreach (list_files($dir) as $file) { foreach (list_files($dir) as $file) {
@ -762,6 +807,29 @@ try {
} }
header('Location: new.php?tab=edit'); header('Location: new.php?tab=edit');
exit; exit;
} elseif ($action === 'reorder_file') {
$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.');
$file = basename((string)($_POST['file'] ?? ''));
$direction = strtolower(trim((string)($_POST['direction'] ?? '')));
$result = move_visual_file_order($dir, $file, $direction);
$message = $result['changed'] ? ('Moved file ' . ($direction === 'up' ? 'up' : 'down') . ': ' . $file) : 'No move possible for file: ' . $file;
if (!empty($_POST['ajax'])) {
ajax_json([
'ok' => true,
'message' => $message,
'file' => $file,
'direction' => $direction,
'changed' => !empty($result['changed']),
'index' => (int)($result['index'] ?? -1),
'count' => (int)($result['count'] ?? 0),
'dir' => basename($dir),
'id' => $articleId,
]);
}
header('Location: new.php?tab=edit&id=' . rawurlencode((string)$articleId) . '&edit=' . rawurlencode(basename($dir)) . '&msg=' . rawurlencode($message));
exit;
} elseif ($action === 'delete_video') { } elseif ($action === 'delete_video') {
$file = basename((string)($_POST['video'] ?? '')); $file = basename((string)($_POST['video'] ?? ''));
$path = $config['videos_dir'] . '/' . $file; $path = $config['videos_dir'] . '/' . $file;
@ -939,11 +1007,138 @@ function deleteFile(name){
.then(function(json){ .then(function(json){
if(!json || !json.ok) throw new Error(json && (json.error || json.message) ? (json.error || json.message) : 'Delete failed'); if(!json || !json.ok) throw new Error(json && (json.error || json.message) ? (json.error || json.message) : 'Delete failed');
if(target) target.remove(); if(target) target.remove();
refreshFileOrderButtons();
showToast(json.message || ('Removed ' + name), true); showToast(json.message || ('Removed ' + name), true);
}).catch(function(err){ }).catch(function(err){
showToast(err.message || String(err), false); showToast(err.message || String(err), false);
}); });
} }
function getEditDirContext(){
var form = document.getElementById('delete-file-form');
if(!form) return {dir:'', id:''};
var dirInput = form.querySelector('input[name=dir]');
var idInput = form.querySelector('input[name=id]');
return {
dir: dirInput ? dirInput.value : '',
id: idInput ? idInput.value : ''
};
}
function captionRowFileName(row){
if(!row) return '';
var strong = row.querySelector('.caption-fields strong') || row.querySelector('strong');
return strong ? strong.textContent.trim() : '';
}
function isVisualMediaName(name){
var lower = String(name || '').toLowerCase();
return /\.(jpg|jpeg|png|webp|gif|mp4|mov|m4v|avi|mkv|webm)$/.test(lower);
}
function getVisualCaptionRows(){
return Array.prototype.slice.call(document.querySelectorAll('.caption-row')).filter(function(row){
return isVisualMediaName(captionRowFileName(row));
});
}
function moveFileOrder(name, direction, row, triggerBtn){
var ctx = getEditDirContext();
if(!ctx.dir){ showToast('Internal error: missing input dir', false); return; }
if(triggerBtn) triggerBtn.disabled = true;
var body = 'action=reorder_file'
+ '&dir=' + encodeURIComponent(ctx.dir)
+ '&id=' + encodeURIComponent(ctx.id)
+ '&file=' + encodeURIComponent(name)
+ '&direction=' + encodeURIComponent(direction)
+ '&ajax=1';
fetch('new.php', {
method: 'POST',
credentials: 'same-origin',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: body
}).then(function(r){ return r.json().catch(function(){ return { ok:false, error:'Invalid server response' }; }); })
.then(function(json){
if(!json || !json.ok) throw new Error((json && (json.error || json.message)) ? (json.error || json.message) : 'Reorder failed');
if(row && json.changed){
var rows = getVisualCaptionRows();
var idx = rows.indexOf(row);
if(direction === 'up' && idx > 0){
row.parentNode.insertBefore(row, rows[idx - 1]);
} else if(direction === 'down' && idx >= 0 && idx < rows.length - 1){
row.parentNode.insertBefore(rows[idx + 1], row);
}
}
refreshFileOrderButtons();
showToast(json.message || 'Order updated', true);
}).catch(function(err){
showToast(err.message || String(err), false);
}).finally(function(){
if(triggerBtn) triggerBtn.disabled = false;
});
}
function ensureFileOrderButtons(){
var rows = document.querySelectorAll('.caption-row');
for(var i=0;i<rows.length;i++){
var row = rows[i];
if(row.dataset.orderButtons === '1') continue;
var fileName = captionRowFileName(row);
if(!isVisualMediaName(fileName)) { row.dataset.orderButtons = '1'; continue; }
var removeBtn = row.querySelector('button[onclick*="deleteFile"]');
if(!removeBtn) { row.dataset.orderButtons = '1'; continue; }
var wrap = document.createElement('span');
wrap.className = 'file-order-controls';
wrap.style.display = 'inline-flex';
wrap.style.gap = '6px';
wrap.style.marginRight = '8px';
var upBtn = document.createElement('button');
upBtn.type = 'button';
upBtn.className = 'button order-btn-up';
upBtn.textContent = '↑';
upBtn.title = 'Move up';
upBtn.style.padding = '0.2rem 0.45rem';
var downBtn = document.createElement('button');
downBtn.type = 'button';
downBtn.className = 'button order-btn-down';
downBtn.textContent = '↓';
downBtn.title = 'Move down';
downBtn.style.padding = '0.2rem 0.45rem';
(function(r, file, up, down){
up.addEventListener('click', function(){ moveFileOrder(file, 'up', r, up); });
down.addEventListener('click', function(){ moveFileOrder(file, 'down', r, down); });
})(row, fileName, upBtn, downBtn);
wrap.appendChild(upBtn);
wrap.appendChild(downBtn);
row.insertBefore(wrap, removeBtn);
row.dataset.orderButtons = '1';
}
}
function refreshFileOrderButtons(){
ensureFileOrderButtons();
var rows = getVisualCaptionRows();
for(var i=0;i<rows.length;i++){
var row = rows[i];
var up = row.querySelector('.order-btn-up');
var down = row.querySelector('.order-btn-down');
if(up) up.disabled = (i === 0);
if(down) down.disabled = (i === rows.length - 1);
}
}
document.addEventListener('DOMContentLoaded', refreshFileOrderButtons);
</script> </script>
<script> <script>
function showToast(message, ok, details){ function showToast(message, ok, details){