Compare commits
2 Commits
43fc752535
...
e6d6ad3c7b
| Author | SHA1 | Date | |
|---|---|---|---|
| e6d6ad3c7b | |||
| 80e3aa95d8 |
9
file.php
9
file.php
@@ -10,6 +10,15 @@ if (!defined('DOKU_INC')) define('DOKU_INC', __DIR__ . '/../../../');
|
||||
if (!defined('DOKU_DISABLE_GZIP_OUTPUT')) define('DOKU_DISABLE_GZIP_OUTPUT', 1); // we gzip ourself here
|
||||
require_once(DOKU_INC . 'inc/init.php');
|
||||
|
||||
// Close the session early to prevent blocking concurrent requests.
|
||||
// PHP sessions are locked by default - if we hold the lock during thumbnail
|
||||
// generation, all other requests from this user (including page navigation)
|
||||
// will be blocked until we finish. Since we only need session data for ACL
|
||||
// checks (which happen before this point via init.php), we can safely close it.
|
||||
if (function_exists('session_status') && session_status() === PHP_SESSION_ACTIVE) {
|
||||
session_write_close();
|
||||
}
|
||||
|
||||
global $INPUT;
|
||||
|
||||
$syntax = plugin_load('syntax', 'luxtools');
|
||||
|
||||
5
images/placeholder.svg
Normal file
5
images/placeholder.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" opacity="0.3">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
|
||||
<circle cx="8.5" cy="8.5" r="1.5"/>
|
||||
<polyline points="21 15 16 10 5 21"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 319 B |
@@ -8,45 +8,100 @@
|
||||
// ============================================================
|
||||
// Gallery Thumbnails Module
|
||||
// ============================================================
|
||||
// Uses fetch() with AbortController to load thumbnails.
|
||||
// This allows true HTTP request cancellation on navigation,
|
||||
// unlike native loading="lazy" where queued requests block.
|
||||
Luxtools.GalleryThumbnails = (function () {
|
||||
function loadThumb(img) {
|
||||
var src = img.getAttribute('data-thumb-src') || '';
|
||||
if (!src) return;
|
||||
if (img.getAttribute('data-thumb-loading') === '1') return;
|
||||
img.setAttribute('data-thumb-loading', '1');
|
||||
var controller = null;
|
||||
var maxConcurrent = 4;
|
||||
var activeCount = 0;
|
||||
var queue = [];
|
||||
|
||||
var pre = new window.Image();
|
||||
pre.onload = function () {
|
||||
img.src = src;
|
||||
img.removeAttribute('data-thumb-src');
|
||||
img.removeAttribute('data-thumb-loading');
|
||||
};
|
||||
pre.onerror = function () {
|
||||
img.removeAttribute('data-thumb-loading');
|
||||
};
|
||||
pre.src = src;
|
||||
function abortAll() {
|
||||
if (controller) {
|
||||
controller.abort();
|
||||
controller = null;
|
||||
}
|
||||
queue = [];
|
||||
activeCount = 0;
|
||||
}
|
||||
|
||||
function processQueue() {
|
||||
if (!controller) return;
|
||||
while (activeCount < maxConcurrent && queue.length > 0) {
|
||||
var img = queue.shift();
|
||||
loadThumb(img);
|
||||
}
|
||||
}
|
||||
|
||||
function loadThumb(img) {
|
||||
if (!controller) return;
|
||||
|
||||
var src = img.getAttribute('data-src');
|
||||
if (!src) {
|
||||
processQueue();
|
||||
return;
|
||||
}
|
||||
|
||||
activeCount++;
|
||||
|
||||
fetch(src, { signal: controller.signal })
|
||||
.then(function (response) {
|
||||
if (!response.ok) throw new Error('HTTP ' + response.status);
|
||||
return response.blob();
|
||||
})
|
||||
.then(function (blob) {
|
||||
img.src = URL.createObjectURL(blob);
|
||||
img.removeAttribute('data-src');
|
||||
})
|
||||
.catch(function (err) {
|
||||
// Aborted or failed - ignore
|
||||
if (err.name !== 'AbortError') {
|
||||
// Keep data-src for potential retry, just log
|
||||
}
|
||||
})
|
||||
.finally(function () {
|
||||
activeCount--;
|
||||
processQueue();
|
||||
});
|
||||
}
|
||||
|
||||
function queueThumb(img) {
|
||||
if (!controller) return;
|
||||
if (!img.getAttribute('data-src')) return;
|
||||
if (img.getAttribute('data-queued') === '1') return;
|
||||
img.setAttribute('data-queued', '1');
|
||||
queue.push(img);
|
||||
processQueue();
|
||||
}
|
||||
|
||||
function init() {
|
||||
// Handle both gallery and imagebox thumbnails
|
||||
var imgs = document.querySelectorAll('div.luxtools-gallery img[data-thumb-src], div.luxtools-imagebox img[data-thumb-src]');
|
||||
var imgs = document.querySelectorAll(
|
||||
'div.luxtools-gallery img.luxtools-thumb[data-src], div.luxtools-imagebox img[data-src]'
|
||||
);
|
||||
if (!imgs || !imgs.length) return;
|
||||
|
||||
// Create abort controller for all requests
|
||||
controller = new AbortController();
|
||||
|
||||
// Abort all pending requests on navigation
|
||||
window.addEventListener('beforeunload', abortAll);
|
||||
window.addEventListener('pagehide', abortAll);
|
||||
|
||||
// Use IntersectionObserver to trigger loading
|
||||
if ('IntersectionObserver' in window) {
|
||||
var io = new window.IntersectionObserver(function (entries) {
|
||||
var io = new IntersectionObserver(function (entries) {
|
||||
entries.forEach(function (entry) {
|
||||
if (!entry.isIntersecting) return;
|
||||
loadThumb(entry.target);
|
||||
queueThumb(entry.target);
|
||||
io.unobserve(entry.target);
|
||||
});
|
||||
}, { rootMargin: '200px' });
|
||||
|
||||
imgs.forEach(function (img) { io.observe(img); });
|
||||
} else {
|
||||
// Fallback: load soon after initial render
|
||||
window.setTimeout(function () {
|
||||
imgs.forEach(loadThumb);
|
||||
}, 0);
|
||||
// Fallback: queue all
|
||||
imgs.forEach(queueThumb);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -67,17 +67,17 @@ class Output
|
||||
$genThumbW = (int)max(1, (int)round($thumbW * $thumbScale));
|
||||
$genThumbH = (int)max(1, (int)round($thumbH * $thumbScale));
|
||||
|
||||
// Build placeholder URL from config (DokuWiki media ID)
|
||||
$placeholderStyle = '';
|
||||
$placeholderId = $syntax ? trim((string)$syntax->getConf('thumb_placeholder')) : '';
|
||||
if ($placeholderId !== '' && function_exists('ml')) {
|
||||
$placeholderUrl = ml($placeholderId, ['w' => $thumbW, 'h' => $thumbH], true, '&');
|
||||
$placeholderStyle = ' style="--luxtools-placeholder: url(' . hsc($placeholderUrl) . ')"';
|
||||
}
|
||||
|
||||
/** @var \Doku_Renderer_xhtml $renderer */
|
||||
$renderer = $this->renderer;
|
||||
$renderer->doc .= '<div class="luxtools-plugin luxtools-gallery" data-luxtools-gallery="1">';
|
||||
|
||||
global $ID;
|
||||
$pageId = isset($ID) ? (string)$ID : '';
|
||||
if (function_exists('cleanID')) {
|
||||
$pageId = (string)cleanID($pageId);
|
||||
}
|
||||
$renderer->doc .= '<div class="luxtools-plugin luxtools-gallery" data-luxtools-gallery="1"' . $placeholderStyle . '>';
|
||||
|
||||
foreach ($this->files as $item) {
|
||||
$url = $this->itemWebUrl($item, !empty($params['randlinks']));
|
||||
@@ -88,39 +88,14 @@ class Output
|
||||
$caption = $label;
|
||||
}
|
||||
|
||||
// Use ThumbnailHelper to get thumbnail info
|
||||
$imagePath = isset($item['path']) && is_string($item['path']) ? $item['path'] : '';
|
||||
if ($imagePath !== '' && is_file($imagePath)) {
|
||||
// Extract root and local from full path for helper
|
||||
$root = $this->basedir;
|
||||
$local = '';
|
||||
if (str_starts_with($imagePath, $root)) {
|
||||
$local = substr($imagePath, strlen($root));
|
||||
}
|
||||
|
||||
$thumb = ThumbnailHelper::getThumbnail(
|
||||
$root,
|
||||
$local,
|
||||
$pageId,
|
||||
$genThumbW,
|
||||
$genThumbH,
|
||||
$thumbQ,
|
||||
$placeholderId !== '' ? $placeholderId : null
|
||||
);
|
||||
|
||||
$initialSrc = hsc($thumb['url']);
|
||||
$dataThumb = $thumb['isFinal'] ? '' : ' data-thumb-src="' . hsc($thumb['thumbUrl']) . '"';
|
||||
} else {
|
||||
// Fallback: use URL directly
|
||||
$thumbUrl = $this->withQueryParams($url, [
|
||||
'thumb' => 1,
|
||||
'w' => $genThumbW,
|
||||
'h' => $genThumbH,
|
||||
'q' => $thumbQ,
|
||||
]);
|
||||
$initialSrc = hsc($thumbUrl);
|
||||
$dataThumb = '';
|
||||
}
|
||||
// Build thumbnail URL - JavaScript will load via fetch() for cancellability
|
||||
$thumbUrl = $this->withQueryParams($url, [
|
||||
'thumb' => 1,
|
||||
'w' => $genThumbW,
|
||||
'h' => $genThumbH,
|
||||
'q' => $thumbQ,
|
||||
]);
|
||||
$thumbSrc = hsc($thumbUrl);
|
||||
|
||||
$renderer->doc .= '<a'
|
||||
. ' href="' . $safeUrl . '"'
|
||||
@@ -132,13 +107,10 @@ class Output
|
||||
. '>';
|
||||
$renderer->doc .= '<img'
|
||||
. ' class="luxtools-thumb"'
|
||||
. ' src="' . $initialSrc . '"'
|
||||
. $dataThumb
|
||||
. ' data-src="' . $thumbSrc . '"'
|
||||
. ' alt=""'
|
||||
. ' width="' . $thumbW . '"'
|
||||
. ' height="' . $thumbH . '"'
|
||||
. ' loading="lazy"'
|
||||
. ' decoding="async"'
|
||||
. ' />';
|
||||
$renderer->doc .= '<span class="luxtools-gallery-caption">' . $caption . '</span>';
|
||||
$renderer->doc .= '</a>';
|
||||
|
||||
10
style.css
10
style.css
@@ -77,6 +77,16 @@ div.luxtools-gallery img.luxtools-thumb {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
object-fit: cover;
|
||||
/* Placeholder while lazy-loaded image is pending.
|
||||
* Uses custom property from inline style if thumb_placeholder is configured,
|
||||
* otherwise falls back to built-in SVG icon. */
|
||||
background-color: @ini_background;
|
||||
background-image: var(--luxtools-placeholder, url(images/placeholder.svg));
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
/* contain works well for both: configured placeholder fills the area,
|
||||
* built-in SVG icon stays small and centered */
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
/* Filename overlay (single line, muted). */
|
||||
|
||||
Reference in New Issue
Block a user