added dynamic loading of favicons

This commit is contained in:
partisan 2025-05-09 08:26:14 +02:00
parent 255acb360f
commit 81fb811111
4 changed files with 355 additions and 35 deletions

View file

@ -50,6 +50,30 @@ var (
iconLinkRegex = regexp.MustCompile(`<link[^>]+rel=["'](?:icon|shortcut icon|apple-touch-icon)["'][^>]+href=["']([^"']+)["']`)
)
// Add this near the top with other vars
var (
faviconDownloadQueue = make(chan faviconDownloadRequest, 1000)
)
type faviconDownloadRequest struct {
faviconURL string
pageURL string
cacheID string
}
func init() {
// Start 5 worker goroutines to process favicon downloads
for i := 0; i < 5; i++ {
go faviconDownloadWorker()
}
}
func faviconDownloadWorker() {
for req := range faviconDownloadQueue {
cacheFavicon(req.faviconURL, req.cacheID)
}
}
// Generates a cache ID from URL
func faviconIDFromURL(rawURL string) string {
hasher := md5.New()
@ -312,14 +336,14 @@ func getFaviconProxyURL(rawFavicon, pageURL string) string {
return fmt.Sprintf("/image/%s_thumb.webp", cacheID)
}
// Resolve URL (but ignore resolved ID — we always use the one from pageURL)
// Resolve URL
faviconURL, _ := resolveFaviconURL(rawFavicon, pageURL)
if faviconURL == "" {
recordInvalidImageID(cacheID)
return "/static/images/missing.svg"
}
// Avoid re-downloading
// Check if already downloading
faviconCache.RLock()
downloading := faviconCache.m[cacheID]
faviconCache.RUnlock()
@ -329,17 +353,12 @@ func getFaviconProxyURL(rawFavicon, pageURL string) string {
faviconCache.m[cacheID] = true
faviconCache.Unlock()
go func() {
defer func() {
faviconCache.Lock()
delete(faviconCache.m, cacheID)
faviconCache.Unlock()
}()
_, _, err := cacheFavicon(faviconURL, cacheID)
if err != nil {
recordInvalidImageID(cacheID)
}
}()
// Send to download queue instead of starting goroutine
faviconDownloadQueue <- faviconDownloadRequest{
faviconURL: faviconURL,
pageURL: pageURL,
cacheID: cacheID,
}
}
return fmt.Sprintf("/image/%s_thumb.webp", cacheID)

View file

@ -0,0 +1,248 @@
(function() {
// Get template data and configuration
const templateData = document.getElementById('template-data');
const type = templateData.getAttribute('data-type');
const hardCacheEnabled = templateData.getAttribute('data-hard-cache-enabled') === 'true';
// Track all favicon/image elements and their IDs
const allMediaElements = [];
const allMediaIds = [];
let statusCheckTimeout = null;
// Add loading effects to image/favicon and associated text
function addLoadingEffects(imgElement) {
const container = imgElement.closest(type === 'image' ? '.image' : '.result_item');
if (!container) return;
const titleSelector = type === 'image' ? '.img_title' : '.result-url';
const title = container.querySelector(titleSelector);
imgElement.closest('.favicon-wrapper')?.classList.add('loading');
// if (title) title.classList.add('title-loading');
}
// Remove loading effects when image/favicon loads
function removeLoadingEffects(imgElement) {
const container = imgElement.closest(type === 'image' ? '.image' : '.result_item');
const titleSelector = type === 'image' ? '.img_title' : '.result-url';
const title = container?.querySelector(titleSelector);
imgElement.closest('.favicon-wrapper')?.classList.remove('loading');
if (title) title.classList.remove('title-loading');
if (type === 'image' && imgElement.src.endsWith('/images/missing.svg')) {
container.remove();
}
}
// Handle image/favicon loading errors
function handleImageError(imgElement) {
const container = imgElement.closest(type === 'image' ? '.image' : '.result_item');
const titleSelector = type === 'image' ? '.img_title' : '.result-url';
const title = container?.querySelector(titleSelector);
imgElement.closest('.favicon-wrapper')?.classList.remove('loading');
if (title) title.classList.remove('title-loading');
if (type === 'image') {
container.style.display = 'none';
} else {
imgElement.src = '/static/images/missing.svg';
}
}
// Shared configuration
const statusCheckInterval = 500;
const scrollThreshold = 500;
const loadingIndicator = document.getElementById('message-bottom-right');
let loadingTimer;
let isFetching = false;
let page = parseInt(templateData.getAttribute('data-page')) || 1;
let query = templateData.getAttribute('data-query');
let noMoreImages = false;
function showLoadingMessage() {
loadingIndicator.classList.add('visible');
}
function hideLoadingMessage() {
loadingIndicator.classList.remove('visible');
}
function ensureScrollable() {
if (noMoreImages) return;
if (document.body.scrollHeight <= window.innerHeight) {
fetchNextPage();
}
}
// Register a new media element for tracking
function registerMediaElement(imgElement) {
const id = imgElement.getAttribute('data-id');
if (!id || allMediaIds.includes(id)) return;
// Wrap the image in a .favicon-wrapper if not already
if (!imgElement.parentElement.classList.contains('favicon-wrapper')) {
const wrapper = document.createElement('span');
wrapper.classList.add('favicon-wrapper');
imgElement.parentElement.replaceChild(wrapper, imgElement);
wrapper.appendChild(imgElement);
}
// Track and style
allMediaElements.push(imgElement);
allMediaIds.push(id);
addLoadingEffects(imgElement);
if (hardCacheEnabled) {
imgElement.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
} else {
imgElement.src = '/static/images/placeholder.svg';
}
// Schedule a status check if not already pending
if (!statusCheckTimeout) {
statusCheckTimeout = setTimeout(checkMediaStatus, statusCheckInterval);
}
}
// Check status of all tracked media elements
function checkMediaStatus() {
statusCheckTimeout = null;
if (allMediaIds.length === 0) return;
// Group IDs to avoid very long URLs
const idGroups = [];
for (let i = 0; i < allMediaIds.length; i += 50) {
idGroups.push(allMediaIds.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 () => {
for (const group of idGroups) {
try {
await checkGroup(group);
} catch (error) {
console.error('Status check error:', error);
}
}
// If we still have pending items, schedule another check
if (allMediaIds.length > 0) {
statusCheckTimeout = setTimeout(checkMediaStatus, statusCheckInterval);
}
};
processGroups();
}
function fetchNextPage() {
if (isFetching || noMoreImages) return;
loadingTimer = setTimeout(() => {
showLoadingMessage();
}, 150);
isFetching = true;
page += 1;
fetch(`/search?q=${encodeURIComponent(query)}&t=${type}&p=${page}&ajax=true`)
.then(response => response.text())
.then(html => {
clearTimeout(loadingTimer);
hideLoadingMessage();
let tempDiv = document.createElement('div');
tempDiv.innerHTML = html;
let newItems = tempDiv.querySelectorAll(type === 'image' ? '.image' : '.result_item');
if (newItems.length > 0) {
let resultsContainer = document.querySelector(type === 'image' ? '.images' : '.results');
newItems.forEach(item => {
let clonedItem = item.cloneNode(true);
resultsContainer.appendChild(clonedItem);
// Register any new media elements
const img = clonedItem.querySelector('img[data-id]');
if (img) {
registerMediaElement(img);
}
});
ensureScrollable();
} else {
noMoreImages = true;
}
isFetching = false;
})
.catch(error => {
clearTimeout(loadingTimer);
hideLoadingMessage();
console.error('Fetch error:', error);
isFetching = false;
});
}
// Initialize all existing media elements
function initializeMediaElements() {
document.querySelectorAll('img[data-id]').forEach(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
if (document.readyState === 'complete') {
initializeMediaElements();
} else {
window.addEventListener('load', initializeMediaElements);
}
// Infinite scroll handler
window.addEventListener('scroll', () => {
if (isFetching || noMoreImages) return;
if (window.innerHeight + window.scrollY >= document.body.offsetHeight - scrollThreshold) {
fetchNextPage();
}
});
// Clean up on page unload
window.addEventListener('beforeunload', () => {
if (statusCheckTimeout) {
clearTimeout(statusCheckTimeout);
}
});
})();

View file

@ -17,6 +17,45 @@
<link rel="icon" href="{{ .IconPathSVG }}" type="image/svg+xml">
<link rel="icon" href="{{ .IconPathPNG }}" type="image/png">
<link rel="apple-touch-icon" href="{{ .IconPathPNG }}">
<style>
.favicon-wrapper {
position: relative;
display: inline-block;
width: 16px;
height: 16px;
}
.favicon-wrapper.loading img {
visibility: hidden; /* hide placeholder */
}
.favicon-wrapper.loading::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
width: 14px;
height: 14px;
margin: -8px 0 0 -8px;
border: 2px solid var(--html-bg);
border-top-color: var(--fg);
border-radius: 50%;
animation: spin 0.7s linear infinite;
z-index: 2;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/*
@keyframes loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
} */
</style>
</head>
<body>
<!-- Menu Button -->
@ -194,10 +233,9 @@
</form>
</div>
<div id="template-data" data-page="{{ .Page }}" data-query="{{ .Query }}" data-type="text"></div>
<script defer src="/static/js/dynamicscrolling.js"></script>
<script defer src="/static/js/dynamicscrollingtext.js"></script>
<script defer src="/static/js/autocomplete.js"></script>
<script defer src="/static/js/minimenu.js"></script>
<script defer src="/static/js/dynamicscrollingimages.js"></script>
<script>
document.querySelectorAll('.js-enabled').forEach(el => el.classList.remove('js-enabled'));
</script>

55
text.go
View file

@ -1,7 +1,10 @@
package main
import (
"fmt"
"net/http"
"os"
"path/filepath"
"time"
)
@ -52,12 +55,11 @@ func HandleTextSearch(w http.ResponseWriter, settings UserSettings, query string
elapsedTime := time.Since(startTime)
// Prepare safe decorated results
// Simplified result structure without waiting for favicons
type DecoratedResult struct {
TextSearchResult
FaviconURL string
FaviconID string
PrettyLink LinkParts
FaviconID string // Just the ID, URL will be generated client-side
}
var decoratedResults []DecoratedResult
@ -66,39 +68,52 @@ func HandleTextSearch(w http.ResponseWriter, settings UserSettings, query string
continue
}
// First format the link
prettyLink := FormatLinkHTML(r.URL)
faviconID := faviconIDFromURL(prettyLink.RootURL)
faviconURL := getFaviconProxyURL("", prettyLink.RootURL)
decoratedResults = append(decoratedResults, DecoratedResult{
TextSearchResult: r,
PrettyLink: prettyLink,
FaviconID: faviconID,
FaviconURL: faviconURL,
})
// Start async favicon fetch if not already cached
go ensureFaviconIsCached(faviconID, prettyLink.RootURL)
}
data := map[string]interface{}{
"Results": decoratedResults,
"Query": query,
"Fetched": FormatElapsedTime(elapsedTime),
"Page": page,
"HasPrevPage": hasPrevPage,
"HasNextPage": len(combinedResults) >= 50,
"NoResults": len(combinedResults) == 0,
"LanguageOptions": languageOptions,
"CurrentLang": settings.SearchLanguage,
"Theme": settings.Theme,
"Safe": settings.SafeSearch,
"IsThemeDark": settings.IsThemeDark,
"Trans": Translate,
"Results": decoratedResults,
"Query": query,
"Fetched": FormatElapsedTime(elapsedTime),
"Page": page,
"HasPrevPage": hasPrevPage,
"HasNextPage": len(combinedResults) >= 50,
"NoResults": len(combinedResults) == 0,
"LanguageOptions": languageOptions,
"CurrentLang": settings.SearchLanguage,
"Theme": settings.Theme,
"Safe": settings.SafeSearch,
"IsThemeDark": settings.IsThemeDark,
"Trans": Translate,
"HardCacheEnabled": config.DriveCacheEnabled,
}
// Render the template
renderTemplate(w, "text.html", data)
}
func ensureFaviconIsCached(faviconID, rootURL string) {
// Check if already exists in cache
filename := fmt.Sprintf("%s_thumb.webp", faviconID)
cachedPath := filepath.Join(config.DriveCache.Path, "images", filename)
if _, err := os.Stat(cachedPath); err == nil {
return // Already cached
}
// Not cached, initiate download
getFaviconProxyURL("", rootURL) // This will trigger async download
}
func getTextResultsFromCacheOrFetch(cacheKey CacheKey, query, safe, lang string, page int) []TextSearchResult {
cacheChan := make(chan []SearchResult)
var combinedResults []TextSearchResult