fix: handle duplicate favicons correctly in search results
All checks were successful
Run Integration Tests / test (push) Successful in 31s

This commit is contained in:
partisan 2025-05-09 11:29:34 +02:00
parent 00bbb5c015
commit 0d5184503d
3 changed files with 87 additions and 92 deletions

View file

@ -1,4 +1,3 @@
// dynamicscrollingimages.js
(function() { (function() {
// Add loading effects to image and title // Add loading effects to image and title
function addLoadingEffects(imgElement) { function addLoadingEffects(imgElement) {
@ -37,7 +36,6 @@
} }
} }
// Rest of your existing code with minor additions
const imageStatusInterval = 500; const imageStatusInterval = 500;
const scrollThreshold = 500; const scrollThreshold = 500;
const loadingIndicator = document.getElementById('message-bottom-right'); let loadingTimer; const loadingIndicator = document.getElementById('message-bottom-right'); let loadingTimer;

View file

@ -7,7 +7,7 @@
// Track all favicon/image elements and their IDs // Track all favicon/image elements and their IDs
let allMediaElements = []; let allMediaElements = [];
let allMediaIds = []; let allMediaIds = [];
let statusCheckTimeout = null; const mediaMap = new Map();
// Add loading effects to image/favicon and associated text // Add loading effects to image/favicon and associated text
function addLoadingEffects(imgElement) { function addLoadingEffects(imgElement) {
@ -34,18 +34,20 @@
} }
// Handle image/favicon loading errors // Handle image/favicon loading errors
function handleImageError(imgElement) { function handleImageError(imgElement, retryCount = 10, retryDelay = 500) {
const container = imgElement.closest(type === 'image' ? '.image' : '.result_item'); const container = imgElement.closest(type === 'image' ? '.image' : '.result_item');
const titleSelector = type === 'image' ? '.img_title' : '.result-url'; const titleSelector = type === 'image' ? '.img_title' : '.result-url';
const title = container?.querySelector(titleSelector); const title = container?.querySelector(titleSelector);
imgElement.closest('.favicon-wrapper')?.classList.remove('loading'); if (retryCount > 0) {
if (title) title.classList.remove('title-loading'); setTimeout(() => {
imgElement.src = imgElement.getAttribute('data-full');
if (type === 'image') { imgElement.onerror = () => handleImageError(imgElement, retryCount - 1, retryDelay);
container.style.display = 'none'; }, retryDelay);
} else { } else {
imgElement.src = '/static/images/missing.svg'; imgElement.closest('.favicon-wrapper')?.classList.remove('loading');
if (title) title.classList.remove('title-loading');
if (type === 'image') container.style.display = 'none';
} }
} }
@ -77,86 +79,78 @@
// Register a new media element for tracking // Register a new media element for tracking
function registerMediaElement(imgElement) { function registerMediaElement(imgElement) {
const id = imgElement.getAttribute('data-id'); const id = imgElement.getAttribute('data-id');
if (!id || allMediaIds.includes(id)) return; if (!id) return;
// Wrap the image in a .favicon-wrapper if not already let wrapper = imgElement.closest('.favicon-wrapper');
if (!imgElement.parentElement.classList.contains('favicon-wrapper')) { if (!wrapper) {
const wrapper = document.createElement('span'); wrapper = document.createElement('span');
wrapper.classList.add('favicon-wrapper'); wrapper.classList.add('favicon-wrapper');
imgElement.parentElement.replaceChild(wrapper, imgElement); imgElement.replaceWith(wrapper);
wrapper.appendChild(imgElement); wrapper.appendChild(imgElement);
} }
// Track and style
allMediaElements.push(imgElement);
allMediaIds.push(id);
addLoadingEffects(imgElement); addLoadingEffects(imgElement);
if (hardCacheEnabled) {
if (!hardCacheEnabled) { imgElement.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
imgElement.src = ''; // don't show anything until actual URL arrives imgElement.onerror = () => handleImageError(imgElement, 3, 1000);
} else {
imgElement.src = imgElement.getAttribute('data-full');
imgElement.onload = () => removeLoadingEffects(imgElement);
imgElement.onerror = () => handleImageError(imgElement, 3, 1000);
} }
// Schedule a status check if not already pending // Track it
if (!statusCheckTimeout) { if (!mediaMap.has(id)) {
statusCheckTimeout = setTimeout(checkMediaStatus, statusCheckInterval); mediaMap.set(id, []);
} }
mediaMap.get(id).push(imgElement);
} }
// Check status of all tracked media elements // Check status of all tracked media elements
function checkMediaStatus() { function checkMediaStatus() {
statusCheckTimeout = null; const allIds = Array.from(mediaMap.keys());
if (allIds.length === 0) return;
if (allMediaIds.length === 0) return;
// Group IDs to avoid very long URLs
const idGroups = []; const idGroups = [];
for (let i = 0; i < allMediaIds.length; i += 50) { for (let i = 0; i < allIds.length; i += 50) {
idGroups.push(allMediaIds.slice(i, i + 50)); idGroups.push(allIds.slice(i, i + 50));
} }
const checkGroup = (group) => {
return fetch(`/image_status?image_ids=${group.join(',')}`)
.then(response => response.json())
.then(statusMap => {
const pendingElements = [];
const pendingIds = [];
allMediaElements.forEach((imgElement, index) => {
const id = allMediaIds[index];
if (group.includes(id)) {
if (statusMap[id]) {
if (imgElement.src !== statusMap[id]) {
imgElement.src = statusMap[id];
imgElement.onload = () => removeLoadingEffects(imgElement);
imgElement.onerror = () => handleImageError(imgElement);
}
} else {
pendingElements.push(imgElement);
pendingIds.push(id);
}
}
});
// Update global arrays with remaining pending items
allMediaElements = pendingElements;
allMediaIds = pendingIds;
});
};
// Process all groups sequentially
const processGroups = async () => { const processGroups = async () => {
const stillPending = new Map();
for (const group of idGroups) { for (const group of idGroups) {
try { try {
await checkGroup(group); const response = await fetch(`/image_status?image_ids=${group.join(',')}`);
} catch (error) { const statusMap = await response.json();
console.error('Status check error:', error);
group.forEach(id => {
const elements = mediaMap.get(id);
const resolved = statusMap[id];
if (resolved && resolved !== 'pending') {
elements.forEach(img => {
img.src = resolved;
img.onload = () => removeLoadingEffects(img);
img.onerror = () => handleImageError(img);
});
} else {
stillPending.set(id, elements);
}
});
} catch (err) {
console.error('Status check failed:', err);
group.forEach(id => {
if (mediaMap.has(id)) {
stillPending.set(id, mediaMap.get(id));
}
});
} }
} }
// If we still have pending items, schedule another check mediaMap.clear();
if (allMediaIds.length > 0) { for (const [id, imgs] of stillPending) {
statusCheckTimeout = setTimeout(checkMediaStatus, statusCheckInterval); mediaMap.set(id, imgs);
} }
}; };
@ -215,18 +209,21 @@
document.querySelectorAll('img[data-id]').forEach(img => { document.querySelectorAll('img[data-id]').forEach(img => {
registerMediaElement(img); registerMediaElement(img);
}); });
// Start periodic checks if hard cache is enabled
if (hardCacheEnabled && allMediaIds.length > 0) {
statusCheckTimeout = setTimeout(checkMediaStatus, statusCheckInterval);
}
} }
// Initialize when DOM is ready function startStatusPolling() {
checkMediaStatus();
setInterval(checkMediaStatus, statusCheckInterval);
}
if (document.readyState === 'complete') { if (document.readyState === 'complete') {
initializeMediaElements(); initializeMediaElements();
if (hardCacheEnabled) startStatusPolling();
} else { } else {
window.addEventListener('load', initializeMediaElements); window.addEventListener('load', () => {
initializeMediaElements();
if (hardCacheEnabled) startStatusPolling();
});
} }
// Infinite scroll handler // Infinite scroll handler
@ -237,10 +234,10 @@
} }
}); });
// Clean up on page unload // // Clean up on page unload
window.addEventListener('beforeunload', () => { // window.addEventListener('beforeunload', () => {
if (statusCheckTimeout) { // if (statusCheckTimeout) {
clearTimeout(statusCheckTimeout); // clearTimeout(statusCheckTimeout);
} // }
}); // });
})(); })();

View file

@ -193,7 +193,7 @@
</noscript> </noscript>
</form> </form>
</div> </div>
<div id="template-data" data-page="{{ .Page }}" data-query="{{ .Query }}" data-type="text"></div> <div id="template-data" data-page="{{ .Page }}" data-query="{{ .Query }}" data-type="text" data-hard-cache-enabled="{{ .HardCacheEnabled }}"></div>
<script defer src="/static/js/dynamicscrollingtext.js"></script> <script defer src="/static/js/dynamicscrollingtext.js"></script>
<script defer src="/static/js/autocomplete.js"></script> <script defer src="/static/js/autocomplete.js"></script>
<script defer src="/static/js/minimenu.js"></script> <script defer src="/static/js/minimenu.js"></script>