diff --git a/index.php b/index.php
index d7c6108..7b61a60 100644
--- a/index.php
+++ b/index.php
@@ -485,6 +485,7 @@ $siteHeaderTitle = $headerTitles[array_rand($headerTitles)];
+
" href="=h(query_url($linkParams))?>">=$i?>
+
diff --git a/new.php b/new.php
index c6fc559..9187e61 100644
--- a/new.php
+++ b/new.php
@@ -75,14 +75,14 @@ function apply_thumbnail_overlay($imagePath, $title, $teaser) {
$text = $normalize($text);
return $text === '' ? '' : wordwrap($text, $width, "\n", false);
};
- $clampWrappedText = static function ($text, int $width, int $maxLines) use ($normalize): string {
+ $clampWrappedText = static function ($text, int $width, int $maxLines, bool $cutLongWords = false) use ($normalize): string {
$text = $normalize($text);
if ($text === '') return '';
$words = preg_split('/\s+/u', $text, -1, PREG_SPLIT_NO_EMPTY) ?: [];
if (!$words) return '';
$candidate = implode(' ', $words);
while (true) {
- $wrapped = wordwrap($candidate, $width, "\n", false);
+ $wrapped = wordwrap($candidate, $width, "\n", $cutLongWords);
$lineCount = substr_count($wrapped, "\n") + 1;
if ($lineCount <= $maxLines || count($words) <= 1) return $wrapped;
array_pop($words);
@@ -94,8 +94,8 @@ function apply_thumbnail_overlay($imagePath, $title, $teaser) {
$bestSize = 95;
$bestWidth = 28;
$bestLines = 1;
- for ($size = 94; $size >= 60; $size -= 4) {
- $width = max(16, min(46, (int)round(24 + (94 - $size) / 2.7)));
+ for ($size = 84; $size >= 54; $size -= 4) {
+ $width = max(16, min(46, (int)round(23 + (84 - $size) / 2.7)));
$candidate = $wrapText($text, $width);
$lines = substr_count($candidate, "\n") + 1;
$bestText = $candidate;
@@ -108,14 +108,14 @@ function apply_thumbnail_overlay($imagePath, $title, $teaser) {
};
[$titleText, $titleFontSize, $titleWrapWidth, $titleLines] = $fitTitleText($title);
- $teaserText = $clampWrappedText($teaser, 38, 6);
+ $teaserText = $clampWrappedText($teaser, 34, 6, true);
$teaserLines = max(1, substr_count($teaserText, "\n") + 1);
- // Move the whole block a bit right while keeping left-aligned text.
- $left = max(18, (int)round($imgW * 0.055));
- $titleY = max(24, (int)round($imgH * 0.06));
+ // Move the whole block a bit left and higher while keeping left-aligned text.
+ $left = max(14, (int)round($imgW * 0.046));
+ $titleY = max(20, (int)round($imgH * 0.062));
$titleLineStep = max(68, (int)round($imgH * 0.043));
- $teaserY = max((int)round($imgH * 0.25), $titleY + ($titleLines * $titleLineStep) + max(56, (int)round($imgH * 0.03)));
+ $teaserY = max((int)round($imgH * 0.235), $titleY + ($titleLines * $titleLineStep) + max(54, (int)round($imgH * 0.03)));
$overlayBase = tempnam(sys_get_temp_dir(), 'thumb_overlay_');
$outputPath = dirname($imagePath) . '/' . basename($imagePath) . '.tmp.' . uniqid('', true) . '.jpg';
@@ -158,7 +158,7 @@ function apply_thumbnail_overlay($imagePath, $title, $teaser) {
foreach ($qualities as $quality) {
$attemptPath = dirname($imagePath) . '/' . basename($imagePath) . '.tmp.' . uniqid('', true) . '.jpg';
$cmd2 = sprintf(
- '/usr/bin/convert -filter Lanczos -define filter:blur=0.85 %s -resize %dx%d^ -gravity center -extent %dx%d -modulate 62,76 %s -composite -strip -interlace Plane -sampling-factor 4:2:0 -quality %d %s 2>&1',
+ '/usr/bin/convert -filter Lanczos -define filter:blur=0.85 %s -resize %dx%d^ -gravity center -extent %dx%d -modulate 70,82 %s -composite -strip -interlace Plane -sampling-factor 4:2:0 -quality %d %s 2>&1',
escapeshellarg($imagePath),
$imgW,
$imgH,
@@ -813,9 +813,13 @@ function input_dir_output($dir){
return basename((string)($state['output'] ?? ''));
}
function regenerate_input_dir_maps($dir, $config){
+ $logFile = '/var/log/mvlog_map.log';
$python = trim((string)shell_exec('command -v python3 2>/dev/null'));
$script = __DIR__ . '/bin/gpsmap.py';
- if ($python === '' || !is_file($script)) return;
+ if ($python === '' || !is_file($script)) {
+ file_put_contents($logFile, '[mvlog] map regenerate skipped for ' . basename((string)$dir) . ' (python=' . ($python !== '' ? 'yes' : 'no') . ', script=' . (is_file($script) ? 'yes' : 'no') . ")\n", FILE_APPEND);
+ return;
+ }
$state = input_dir_state($dir);
$targets = [];
@@ -831,14 +835,19 @@ function regenerate_input_dir_maps($dir, $config){
}
$targets = array_values(array_unique($targets));
+ file_put_contents($logFile, '[mvlog] map regenerate requested for ' . basename((string)$dir) . ' targets=' . implode(', ', array_map('basename', $targets)) . "\n", FILE_APPEND);
foreach ($targets as $outPng) {
+ file_put_contents($logFile, '[mvlog] map regenerate start for ' . basename((string)$dir) . ' target=' . basename((string)$outPng) . "\n", FILE_APPEND);
$cmd = escapeshellarg($python) . ' ' . escapeshellarg($script) . ' ' . escapeshellarg($dir) . ' ' . escapeshellarg($outPng) . ' 2>&1';
$output = (string)shell_exec($cmd);
if (is_file($outPng)) {
@chmod($outPng, 0664);
+ clearstatcache(true, $outPng);
+ $size = @filesize($outPng);
+ file_put_contents($logFile, '[mvlog] map regenerate done for ' . basename((string)$dir) . ' target=' . basename((string)$outPng) . ' bytes=' . ($size !== false ? $size : 'unknown') . "\n", FILE_APPEND);
} else {
- error_log('[mvlog] map regenerate failed for '.basename((string)$dir).' target='.basename((string)$outPng).' output='.trim($output)."\n", 3, '/var/log/mvlog_map.log');
+ file_put_contents($logFile, '[mvlog] map regenerate failed for ' . basename((string)$dir) . ' target=' . basename((string)$outPng) . ' output=' . trim($output) . "\n", FILE_APPEND);
}
}
}
@@ -1099,11 +1108,15 @@ try {
$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.');
- write_data($dir, $_POST);
- save_uploads('media', $dir, $allowedMedia);
- save_uploads('audio', $dir, $allowedAudio);
- // Always regenerate map image(s) on Save changes.
- regenerate_input_dir_maps($dir, $config);
+ file_put_contents('/var/log/mvlog_map.log', '[mvlog] save changes pressed for ' . basename($dir) . ' id=' . $articleId . "\n", FILE_APPEND);
+ try {
+ write_data($dir, $_POST);
+ save_uploads('media', $dir, $allowedMedia);
+ save_uploads('audio', $dir, $allowedAudio);
+ } finally {
+ // Always regenerate map image(s) whenever Save changes is pressed.
+ 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), 'id'=>$articleId]);
header('Location: new.php?tab=edit');
exit;
@@ -1368,9 +1381,11 @@ function setThumbButtonsDisabled(disabled){
buttons[i].classList.toggle('is-busy-target', disabled);
}
}
-function showThumbBusy(){
+function showThumbBusy(label){
var overlay = ensureThumbBusyOverlay();
setThumbButtonsDisabled(true);
+ var text = overlay.querySelector('.label');
+ if(text && label) text.textContent = label;
overlay.classList.add('show');
}
function hideThumbBusy(){
@@ -1380,7 +1395,7 @@ function hideThumbBusy(){
function setThumb(name){
var ctx = getEditDirContext();
if(!ctx.dir){ showToast('Internal error: missing input dir', false); return; }
- showThumbBusy();
+ showThumbBusy('Creating thumbnail…');
var body = 'action=set_video_thumb&dir=' + encodeURIComponent(ctx.dir) + '&id=' + encodeURIComponent(ctx.id) + '&file=' + encodeURIComponent(name) + '&ajax=1';
fetch('new.php', {
method: 'POST',
@@ -1617,7 +1632,7 @@ function handleAjaxFormSubmit(form, redirectTab) {
data.set('ajax', '1');
const action = String(data.get('action') || '');
- const verb = action === 'update' ? 'Saving'
+ const verb = action === 'update' ? 'Saving changes…'
: action === 'delete_video' ? 'Deleting'
: action === 'delete_dir' ? 'Deleting'
: action === 'delete_file' ? 'Deleting'
@@ -1626,18 +1641,22 @@ function handleAjaxFormSubmit(form, redirectTab) {
let timer = 0;
let success = false;
+ const useBusyOverlay = action === 'update';
if (submit) {
submit.dataset.label = submit.dataset.label || submit.textContent.trim() || 'Submit';
submit.disabled = true;
submit.style.opacity = '0.78';
submit.style.cursor = 'wait';
- let dots = 0;
- submit.textContent = verb;
- timer = window.setInterval(function(){
- dots = (dots + 1) % 4;
- submit.textContent = verb + '.'.repeat(dots);
- }, 320);
+ if (!useBusyOverlay) {
+ let dots = 0;
+ submit.textContent = verb;
+ timer = window.setInterval(function(){
+ dots = (dots + 1) % 4;
+ submit.textContent = verb + '.'.repeat(dots);
+ }, 320);
+ }
}
+ if (useBusyOverlay) showThumbBusy('Saving changes…');
try{
const res = await fetch('new.php', { method: 'POST', body: data, credentials: 'same-origin', headers: { 'Accept': 'application/json' }});
@@ -1645,9 +1664,11 @@ 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 = 'new.php?tab=' + (json.tab || redirectTab); }, 450);
} catch(e) {
showToast(e.message || 'Operation failed', false);
+ if (useBusyOverlay) setTimeout(hideThumbBusy, 120);
} finally {
if (timer) window.clearInterval(timer);
if(submit && !success) {
@@ -1656,6 +1677,7 @@ function handleAjaxFormSubmit(form, redirectTab) {
submit.style.cursor = '';
submit.textContent = submit.dataset.label || 'Submit';
}
+ if (useBusyOverlay && !success) hideThumbBusy();
}
});
}