diff --git a/cache-images.go b/cache-images.go index 26e1abc..5fa2251 100644 --- a/cache-images.go +++ b/cache-images.go @@ -25,14 +25,31 @@ import ( var ( cachingImages = make(map[string]*sync.Mutex) cachingImagesMu sync.Mutex - cachingSemaphore = make(chan struct{}, 30) // Limit to concurrent downloads + cachingSemaphore = make(chan struct{}, 100) // Limit to concurrent downloads invalidImageIDs = make(map[string]struct{}) invalidImageIDsMu sync.Mutex + + imageURLMap = make(map[string]string) // mapping from imageID_type to imageURL + imageURLMapMu sync.RWMutex // mutex for thread-safe access ) -func cacheImage(imageURL, filename, imageID string) (string, bool, error) { +func cacheImage(imageURL, imageID string, isThumbnail bool) (string, bool, error) { cacheDir := "image_cache" + + if imageURL == "" { + recordInvalidImageID(imageID) + return "", false, fmt.Errorf("empty image URL for image ID %s", imageID) + } + + // Construct the filename based on the image ID and type + var filename string + if isThumbnail { + filename = fmt.Sprintf("%s_thumb.webp", imageID) + } else { + filename = fmt.Sprintf("%s_full.webp", imageID) + } + cachedImagePath := filepath.Join(cacheDir, filename) tempImagePath := cachedImagePath + ".tmp" @@ -181,46 +198,105 @@ func cacheImage(imageURL, filename, imageID string) (string, bool, error) { return cachedImagePath, true, nil } -func handleCachedImages(w http.ResponseWriter, r *http.Request) { +func handleImageServe(w http.ResponseWriter, r *http.Request) { + // Extract the image ID and type from the URL imageName := filepath.Base(r.URL.Path) + idType := imageName + + var imageID, imageType string + + hasExtension := false + if strings.HasSuffix(idType, ".webp") { + // Cached image, remove extension + idType = strings.TrimSuffix(idType, ".webp") + hasExtension = true + } + + parts := strings.SplitN(idType, "_", 2) + if len(parts) != 2 { + http.NotFound(w, r) + return + } + imageID = parts[0] + imageType = parts[1] + cacheDir := "image_cache" - cachedImagePath := filepath.Join(cacheDir, imageName) + filename := fmt.Sprintf("%s_%s.webp", imageID, imageType) + cachedImagePath := filepath.Join(cacheDir, filename) - if _, err := os.Stat(cachedImagePath); os.IsNotExist(err) { - printDebug("Cached image not found: %s, serving missing.svg", cachedImagePath) - // Serve missing image - missingImagePath := filepath.Join("static", "images", "missing.svg") - w.Header().Set("Content-Type", "image/svg+xml") - http.ServeFile(w, r, missingImagePath) - return - } else if err != nil { - printWarn("Error checking image file: %v", err) - http.Error(w, "Internal Server Error", http.StatusInternalServerError) + if hasExtension && imageType == "thumb" { + // Requesting cached thumbnail image + if _, err := os.Stat(cachedImagePath); err == nil { + // Cached image exists, serve it + contentType := "image/webp" + w.Header().Set("Content-Type", contentType) + w.Header().Set("Cache-Control", "public, max-age=31536000") + http.ServeFile(w, r, cachedImagePath) + return + } else { + // Cached image not found + if config.HardCacheEnabled { + // Thumbnail should be cached, but not found + serveMissingImage(w, r) + return + } + // Else, proceed to proxy (if HardCacheEnabled is false) + } + } + + // For full images, proceed to proxy the image + + // Image not cached or caching not enabled + imageKey := fmt.Sprintf("%s_%s", imageID, imageType) + + imageURLMapMu.RLock() + imageURL, exists := imageURLMap[imageKey] + imageURLMapMu.RUnlock() + + if !exists { + // Cannot find original URL, serve missing image + serveMissingImage(w, r) return } - // Determine the content type based on the file extension - extension := strings.ToLower(filepath.Ext(cachedImagePath)) - var contentType string - switch extension { - case ".svg": - contentType = "image/svg+xml" - case ".jpg", ".jpeg": - contentType = "image/jpeg" - case ".png": - contentType = "image/png" - case ".gif": - contentType = "image/gif" - case ".webp": - contentType = "image/webp" - default: - // Default to binary stream if unknown - contentType = "application/octet-stream" + // For thumbnails, if HardCacheEnabled is true, and image not cached, serve missing image + if imageType == "thumb" && config.HardCacheEnabled { + // Thumbnail should be cached, but not found + serveMissingImage(w, r) + return } - w.Header().Set("Content-Type", contentType) - w.Header().Set("Cache-Control", "public, max-age=31536000") // Cache the image for 1 year - http.ServeFile(w, r, cachedImagePath) + // For full images, proceed to proxy the image + + // Fetch the image from the original URL + resp, err := http.Get(imageURL) + if err != nil { + printWarn("Error fetching image: %v", err) + serveMissingImage(w, r) + return + } + defer resp.Body.Close() + + // Check if the request was successful + if resp.StatusCode != http.StatusOK { + serveMissingImage(w, r) + return + } + + // Set the Content-Type header to the type of the fetched image + contentType := resp.Header.Get("Content-Type") + if contentType != "" && strings.HasPrefix(contentType, "image/") { + w.Header().Set("Content-Type", contentType) + } else { + serveMissingImage(w, r) + return + } + + // Write the image content to the response + if _, err := io.Copy(w, resp.Body); err != nil { + printWarn("Error writing image to response: %v", err) + } + return } func handleImageStatus(w http.ResponseWriter, r *http.Request) { @@ -228,46 +304,55 @@ func handleImageStatus(w http.ResponseWriter, r *http.Request) { ids := strings.Split(imageIDs, ",") statusMap := make(map[string]string) - cacheDir := "image_cache" - - printDebug("Received image status request for IDs: %v", ids) - - invalidImageIDsMu.Lock() - defer invalidImageIDsMu.Unlock() for _, id := range ids { - // Check if the image ID is in the invalidImageIDs map - if _, invalid := invalidImageIDs[id]; invalid { - // Image is invalid, set status to "missing" - statusMap[id] = "/static/images/missing.svg" + if id == "" { continue } - // Check for different possible extensions - extensions := []string{".webp", ".svg"} - var cachedImagePath string - var found bool + // Check for cached full or thumbnail images + cacheDir := "image_cache" + extensions := []string{"webp", "svg"} // Extensions without leading dots + imageReady := false + // Check thumbnail first for _, ext := range extensions { - filename := id + ext - path := filepath.Join(cacheDir, filename) - if _, err := os.Stat(path); err == nil { - cachedImagePath = "/image_cache/" + filename - found = true + thumbFilename := fmt.Sprintf("%s_thumb.%s", id, ext) + thumbPath := filepath.Join(cacheDir, thumbFilename) + + if _, err := os.Stat(thumbPath); err == nil { + statusMap[id] = fmt.Sprintf("/image/%s_thumb.%s", id, ext) + imageReady = true break } } - if found { - statusMap[id] = cachedImagePath - } else { - // Image is not ready yet - statusMap[id] = "" + // If no thumbnail, check full image + if !imageReady { + for _, ext := range extensions { + fullFilename := fmt.Sprintf("%s_full.%s", id, ext) + fullPath := filepath.Join(cacheDir, fullFilename) + + if _, err := os.Stat(fullPath); err == nil { + statusMap[id] = fmt.Sprintf("/image/%s_full.%s", id, ext) + imageReady = true + break + } + } + } + + // If neither is ready + if !imageReady { + if !config.HardCacheEnabled { + // Hard cache is disabled; use the proxy URL + statusMap[id] = fmt.Sprintf("/image/%s_thumb", id) + } else { + // Hard cache is enabled; image is not yet cached + // Do not set statusMap[id]; the frontend will keep checking + } } } - printDebug("Status map: %v", statusMap) - w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(statusMap) } @@ -338,3 +423,20 @@ func removeImageResultFromCache(query string, page int, safe bool, lang string, delete(rc.results, keyStr) } } + +func getContentType(ext string) string { + switch strings.ToLower(ext) { + case "svg": + return "image/svg+xml" + case "jpg", "jpeg": + return "image/jpeg" + case "png": + return "image/png" + case "gif": + return "image/gif" + case "webp": + return "image/webp" + default: + return "application/octet-stream" + } +} diff --git a/imageproxy.go b/imageproxy.go index fcacac4..8451ed4 100644 --- a/imageproxy.go +++ b/imageproxy.go @@ -7,26 +7,19 @@ import ( "strings" ) -func handleImageProxy(w http.ResponseWriter, r *http.Request) { - // Get the URL of the image from the query string - imageURL := r.URL.Query().Get("url") - if imageURL == "" { - http.Error(w, "URL parameter is required", http.StatusBadRequest) - return - } - +func serveImageProxy(w http.ResponseWriter, imageURL string) { // Fetch the image from the external URL resp, err := http.Get(imageURL) if err != nil { printWarn("Error fetching image: %v", err) - serveMissingImage(w, r) + serveMissingImage(w, nil) return } defer resp.Body.Close() // Check if the request was successful if resp.StatusCode != http.StatusOK { - serveMissingImage(w, r) + serveMissingImage(w, nil) return } @@ -35,7 +28,7 @@ func handleImageProxy(w http.ResponseWriter, r *http.Request) { if contentType != "" && strings.HasPrefix(contentType, "image/") { w.Header().Set("Content-Type", contentType) } else { - serveMissingImage(w, r) + serveMissingImage(w, nil) return } diff --git a/images-bing.go b/images-bing.go index 2aa0fbf..b6a6aa6 100644 --- a/images-bing.go +++ b/images-bing.go @@ -5,7 +5,6 @@ import ( "fmt" "net/http" "net/url" - "strconv" "strings" "time" @@ -39,27 +38,13 @@ func PerformBingImageSearch(query, safe, lang string, page int) ([]ImageSearchRe // Extract data using goquery var results []ImageSearchResult doc.Find(".iusc").Each(func(i int, s *goquery.Selection) { - // Extract image source - imgTag := s.Find("img") - imgSrc, exists := imgTag.Attr("src") - if !exists { - imgSrc, exists = imgTag.Attr("data-src") - if !exists { - return - } - } - - // Extract width and height if available - width, _ := strconv.Atoi(imgTag.AttrOr("width", "0")) - height, _ := strconv.Atoi(imgTag.AttrOr("height", "0")) - // Extract the m parameter (JSON-encoded image metadata) metadata, exists := s.Attr("m") if !exists { return } - // Parse the metadata to get the media URL and title + // Parse the metadata to get the direct image URL and title var data map[string]interface{} if err := json.Unmarshal([]byte(metadata), &data); err == nil { mediaURL, ok := data["murl"].(string) @@ -67,21 +52,45 @@ func PerformBingImageSearch(query, safe, lang string, page int) ([]ImageSearchRe return } + imgURL, ok := data["imgurl"].(string) + if !ok { + imgURL = mediaURL // Fallback to mediaURL if imgurl is not available + } + + // Use imgURL as the direct image URL + directImageURL := imgURL + // Extract title from the metadata title, _ := data["t"].(string) - // Apply the image proxy - proxiedFullURL := "/imgproxy?url=" + mediaURL - proxiedThumbURL := "/imgproxy?url=" + imgSrc + // Extract dimensions if available + width := 0 + height := 0 + if ow, ok := data["ow"].(float64); ok { + width = int(ow) + } + if oh, ok := data["oh"].(float64); ok { + height = int(oh) + } + + // Extract thumbnail URL from the 'turl' field + thumbURL, _ := data["turl"].(string) + if thumbURL == "" { + // As a fallback, try to get it from the 'src' or 'data-src' attributes + imgTag := s.Find("img") + thumbURL, exists = imgTag.Attr("src") + if !exists { + thumbURL, _ = imgTag.Attr("data-src") + } + } + results = append(results, ImageSearchResult{ - Thumb: imgSrc, - Title: strings.TrimSpace(title), - Full: imgSrc, - Source: mediaURL, - ProxyFull: proxiedFullURL, // Proxied full-size image URL - ProxyThumb: proxiedThumbURL, // Proxied thumbnail URL - Width: width, - Height: height, + Thumb: thumbURL, + Title: strings.TrimSpace(title), + Full: directImageURL, + Source: mediaURL, + Width: width, + Height: height, }) } }) diff --git a/images-deviantart.go b/images-deviantart.go index 4efe296..3077640 100644 --- a/images-deviantart.go +++ b/images-deviantart.go @@ -151,13 +151,11 @@ func PerformDeviantArtImageSearch(query, safe, lang string, page int) ([]ImageSe // Verify if the image URL is accessible if DeviantArtisValidImageURL(imgSrc, DeviantArtImageUserAgent, resultURL) { resultsChan <- ImageSearchResult{ - Title: strings.TrimSpace(title), - Full: imgSrc, - Width: 0, - Height: 0, - Source: resultURL, - ProxyThumb: "/imgproxy?url=" + imgSrc, // Proxied thumbnail - ProxyFull: "/imgproxy?url=" + imgSrc, // Proxied full-size image + Title: strings.TrimSpace(title), + Full: imgSrc, + Width: 0, + Height: 0, + Source: resultURL, } } }(imgSrc, resultURL, title) diff --git a/images-imgur.go b/images-imgur.go index 826b48e..641f645 100644 --- a/images-imgur.go +++ b/images-imgur.go @@ -64,19 +64,13 @@ func PerformImgurImageSearch(query, safe, lang string, page int) ([]ImageSearchR width, _ := strconv.Atoi(s.Find("a img").AttrOr("width", "0")) height, _ := strconv.Atoi(s.Find("a img").AttrOr("height", "0")) - // Generate proxied URLs - proxyFullURL := "/imgproxy?url=" + url.QueryEscape(imgSrc) - proxyThumbURL := "/imgproxy?url=" + url.QueryEscape(thumbnailSrc) - results = append(results, ImageSearchResult{ - Thumb: thumbnailSrc, - Title: strings.TrimSpace(title), - Full: imgSrc, - Width: width, - Height: height, - Source: "https://imgur.com" + urlPath, - ProxyFull: proxyFullURL, - ProxyThumb: proxyThumbURL, + Thumb: thumbnailSrc, + Title: strings.TrimSpace(title), + Full: imgSrc, + Width: width, + Height: height, + Source: "https://imgur.com" + urlPath, }) }) diff --git a/images-quant.go b/images-quant.go index 67a7ae1..d85d0f9 100644 --- a/images-quant.go +++ b/images-quant.go @@ -159,14 +159,12 @@ func PerformQwantImageSearch(query, safe, lang string, page int) ([]ImageSearchR // Populate the result results[i] = ImageSearchResult{ - Thumb: item.Media, // item.Thumbnail is not working - Title: item.Title, - Full: item.Media, - Source: item.Url, - ProxyFull: "/imgproxy?url=" + item.Media, - ProxyThumb: "/imgproxy?url=" + item.Media, - Width: item.Width, - Height: item.Height, + Thumb: item.Media, // item.Thumbnail is not working + Title: item.Title, + Full: item.Media, + Source: item.Url, + Width: item.Width, + Height: item.Height, } }(i, item) } diff --git a/images.go b/images.go index 6743060..420fe21 100755 --- a/images.go +++ b/images.go @@ -58,6 +58,12 @@ func handleImageSearch(w http.ResponseWriter, r *http.Request, settings UserSett "JsDisabled": jsDisabled, } + if r.URL.Query().Get("ajax") == "true" { + // Render only the images + renderTemplate(w, "images_only.html", data) + return + } + // Render the full page renderTemplate(w, "images.html", data) } @@ -104,7 +110,6 @@ func getImageResultsFromCacheOrFetch(cacheKey CacheKey, query, safe, lang string func fetchImageResults(query, safe, lang string, page int, synchronous bool) []ImageSearchResult { var results []ImageSearchResult engineCount := len(imageSearchEngines) - safeBool := safe == "active" // Determine the engine to use based on the page number engineIndex := (page - 1) % engineCount @@ -120,37 +125,47 @@ func fetchImageResults(query, safe, lang string, page int, synchronous bool) []I } else { for _, result := range searchResults { imageResult := result.(ImageSearchResult) - if config.HardCacheEnabled { - // Generate hash and set up caching - hasher := md5.New() - hasher.Write([]byte(imageResult.Full)) - hash := hex.EncodeToString(hasher.Sum(nil)) - filename := hash + ".webp" - imageResult.ID = hash - imageResult.ProxyFull = "/image_cache/" + filename - if synchronous { - // Synchronously cache the image - _, success, err := cacheImage(imageResult.Full, filename, imageResult.ID) - if err != nil || !success { - printWarn("Failed to cache image %s: %v", imageResult.Full, err) - // Fallback to proxy URL - imageResult.ProxyFull = "/imgproxy?url=" + imageResult.Full - } - } else { - // Start caching and validation in the background - go func(imgResult ImageSearchResult, originalURL, filename string) { - _, success, err := cacheImage(originalURL, filename, imgResult.ID) - if err != nil || !success { - printWarn("Failed to cache image %s: %v", originalURL, err) - removeImageResultFromCache(query, page, safeBool, lang, imgResult.ID) - } - }(imageResult, imageResult.Full, filename) - } - } else { - // Use proxied URLs when hard cache is disabled - imageResult.ProxyFull = "/imgproxy?url=" + imageResult.Full + // Skip image if thumbnail URL is empty + if imageResult.Thumb == "" { + printWarn("Skipping image with empty thumbnail URL. Full URL: %s", imageResult.Full) + continue } + + // Generate hash and set up caching + hasher := md5.New() + hasher.Write([]byte(imageResult.Full)) + hash := hex.EncodeToString(hasher.Sum(nil)) + imageResult.ID = hash + + // Store mapping from imageID_full and imageID_thumb to URLs + imageURLMapMu.Lock() + imageURLMap[fmt.Sprintf("%s_full", hash)] = imageResult.Full + imageURLMap[fmt.Sprintf("%s_thumb", hash)] = imageResult.Thumb + imageURLMapMu.Unlock() + + // Set ProxyFull and ProxyThumb + if config.HardCacheEnabled { + // Cache the thumbnail image asynchronously + go func(imgResult ImageSearchResult) { + _, success, err := cacheImage(imgResult.Thumb, imgResult.ID, true) + if err != nil || !success { + printWarn("Failed to cache thumbnail image %s: %v", imgResult.Thumb, err) + removeImageResultFromCache(query, page, safe == "active", lang, imgResult.ID) + } + }(imageResult) + + // Set ProxyThumb to the proxy URL (initially placeholder) + imageResult.ProxyThumb = fmt.Sprintf("/image/%s_thumb.webp", hash) + + // Set ProxyFull to the proxy URL + imageResult.ProxyFull = fmt.Sprintf("/image/%s_full", hash) + } else { + // Hard cache disabled, proxy both thumb and full images + imageResult.ProxyThumb = fmt.Sprintf("/image/%s_thumb", hash) + imageResult.ProxyFull = fmt.Sprintf("/image/%s_full", hash) + } + results = append(results, imageResult) } } @@ -170,44 +185,46 @@ func fetchImageResults(query, safe, lang string, page int, synchronous bool) []I } for _, result := range searchResults { imageResult := result.(ImageSearchResult) - if config.HardCacheEnabled { - // Generate hash and set up caching - hasher := md5.New() - hasher.Write([]byte(imageResult.Full)) - hash := hex.EncodeToString(hasher.Sum(nil)) - filename := hash + ".webp" - imageResult.ID = hash - imageResult.ProxyFull = "/image_cache/" + filename - if synchronous { - // Synchronously cache the image - _, success, err := cacheImage(imageResult.Full, filename, imageResult.ID) - if err != nil { - printWarn("Failed to cache image %s: %v", imageResult.Full, err) - // Skip this image - continue - } - if !success { - // Skip this image - continue - } - } else { - // Start caching and validation in the background - go func(imgResult ImageSearchResult, originalURL, filename string) { - _, success, err := cacheImage(originalURL, filename, imgResult.ID) - if err != nil { - printWarn("Failed to cache image %s: %v", originalURL, err) - } - if !success { - removeImageResultFromCache(query, page, safeBool, lang, imgResult.ID) - } - }(imageResult, imageResult.Full, filename) - } - } else { - // Use proxied URLs when hard cache is disabled - imageResult.ProxyThumb = "/imgproxy?url=" + imageResult.Thumb - imageResult.ProxyFull = "/imgproxy?url=" + imageResult.Full + // Skip image if thumbnail URL is empty + if imageResult.Thumb == "" { + printWarn("Skipping image with empty thumbnail URL. Full URL: %s", imageResult.Full) + continue } + + // Generate hash and set up caching + hasher := md5.New() + hasher.Write([]byte(imageResult.Full)) + hash := hex.EncodeToString(hasher.Sum(nil)) + imageResult.ID = hash + + // Store mapping from imageID_full and imageID_thumb to URLs + imageURLMapMu.Lock() + imageURLMap[fmt.Sprintf("%s_full", hash)] = imageResult.Full + imageURLMap[fmt.Sprintf("%s_thumb", hash)] = imageResult.Thumb + imageURLMapMu.Unlock() + + if config.HardCacheEnabled { + // Cache the thumbnail image asynchronously + go func(imgResult ImageSearchResult) { + _, success, err := cacheImage(imgResult.Thumb, imgResult.ID, true) + if err != nil || !success { + printWarn("Failed to cache thumbnail image %s: %v", imgResult.Thumb, err) + removeImageResultFromCache(query, page, safe == "active", lang, imgResult.ID) + } + }(imageResult) + + // Set ProxyThumb to the proxy URL (initially placeholder) + imageResult.ProxyThumb = fmt.Sprintf("/image/%s_thumb.webp", hash) + + // Set ProxyFull to the proxy URL + imageResult.ProxyFull = fmt.Sprintf("/image/%s_full", hash) + } else { + // Hard cache disabled, proxy both thumb and full images + imageResult.ProxyThumb = fmt.Sprintf("/image/%s_thumb", hash) + imageResult.ProxyFull = fmt.Sprintf("/image/%s_full", hash) + } + results = append(results, imageResult) } @@ -217,20 +234,20 @@ func fetchImageResults(query, safe, lang string, page int, synchronous bool) []I } } - // Filter out images that failed to cache or are invalid - validResults := make([]ImageSearchResult, 0, len(results)) - for _, imageResult := range results { - if imageResult.ProxyFull != "" { - validResults = append(validResults, imageResult) - } else { - printWarn("Skipping invalid image with ID %s", imageResult.ID) - } - } + // // Filter out images that failed to cache or are invalid + // validResults := make([]ImageSearchResult, 0, len(results)) + // for _, imageResult := range results { + // if imageResult.ProxyFull != "" { + // validResults = append(validResults, imageResult) + // } else { + // printWarn("Skipping invalid image with ID %s", imageResult.ID) + // } + // } // Final debug print to show the count of results fetched printInfo("Fetched %d image results for overall page %d", len(results), page) - return validResults + return results } func wrapImageSearchFunc(f func(string, string, string, int) ([]ImageSearchResult, time.Duration, error)) func(string, string, string, int) ([]SearchResult, time.Duration, error) { diff --git a/main.go b/main.go index 85fa213..51c2456 100755 --- a/main.go +++ b/main.go @@ -214,11 +214,12 @@ func runServer() { http.HandleFunc("/", handleSearch) http.HandleFunc("/search", handleSearch) http.HandleFunc("/suggestions", handleSuggestions) - http.HandleFunc("/imgproxy", handleImageProxy) + // The /imgproxy handler is deprecated, now its handled by /image/ + // http.HandleFunc("/imgproxy", handleImageProxy) http.HandleFunc("/node", handleNodeRequest) http.HandleFunc("/settings", handleSettings) http.HandleFunc("/save-settings", handleSaveSettings) - http.HandleFunc("/image_cache/", handleCachedImages) + http.HandleFunc("/image/", handleImageServe) http.HandleFunc("/image_status", handleImageStatus) http.HandleFunc("/opensearch.xml", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/opensearchdescription+xml") diff --git a/static/css/style-search.css b/static/css/style-search.css index e5865cc..839deaf 100644 --- a/static/css/style-search.css +++ b/static/css/style-search.css @@ -44,8 +44,9 @@ body, html { color: var(--text-color); } -body { - visibility: hidden; +button, p { + font-family: 'Inter', Arial, Helvetica, sans-serif; + font-weight: 400; } body.menu-open { @@ -259,6 +260,7 @@ body.menu-open { } .settings-icon-link-search:hover { + text-decoration: none; color: var(--blue); transition: color 0.3s ease; } diff --git a/static/js/imageviewer.js b/static/js/imageviewer.js index ca54778..d703741 100644 --- a/static/js/imageviewer.js +++ b/static/js/imageviewer.js @@ -1,21 +1,28 @@ document.addEventListener('DOMContentLoaded', function() { let viewerOpen = false; + let currentIndex = -1; + let imageList = []; + + // Initialize imageList with all images on the page + function initializeImageList() { + imageList = Array.from(document.querySelectorAll('.image img.clickable')); + } const viewerOverlay = document.getElementById('image-viewer-overlay'); viewerOverlay.innerHTML = `
- - +
- +
@@ -23,8 +30,8 @@ document.addEventListener('DOMContentLoaded', function() {

View source:

- Show source website - Show in fullscreen + Show source website + Show in fullscreen

`; @@ -32,38 +39,47 @@ document.addEventListener('DOMContentLoaded', function() { const imageView = document.getElementById('image-viewer'); function openImageViewer(element) { + initializeImageList(); // Update the image list + const parentImageDiv = element.closest('.image'); if (!parentImageDiv) return; - const imgElement = parentImageDiv.querySelector('img.clickable'); - const fullImageUrl = imgElement.dataset.proxyFull; // Use data-proxy-full for ProxyFull - const thumbnailUrl = imgElement.src; // Use ProxyThumb for the thumbnail - const title = imgElement.alt; - const sourceUrl = parentImageDiv.querySelector('.img_source').href; // Source webpage URL + currentIndex = imageList.findIndex(img => img === parentImageDiv.querySelector('img.clickable')); + if (currentIndex === -1) return; - if (!fullImageUrl || viewerOpen) { - return; // Don't open if data is missing or viewer is already open - } + displayImage(currentIndex); viewerOpen = true; - const viewerImage = document.getElementById('viewer-image'); - const viewerTitle = document.getElementById('viewer-title'); - const viewerSourceButton = document.getElementById('viewer-source-button'); - const fullSizeLink = imageView.querySelector('.full-size'); - const proxySizeLink = imageView.querySelector('.proxy-size'); - - // Set the viewer image to ProxyFull - viewerImage.src = fullImageUrl; - viewerTitle.textContent = title; - viewerSourceButton.href = sourceUrl; - fullSizeLink.href = sourceUrl; // Link to the source website - proxySizeLink.href = fullImageUrl; // Link to the proxied full-size image - viewerOverlay.style.display = 'flex'; imageView.classList.remove('image_hide'); imageView.classList.add('image_show'); } + function displayImage(index) { + if (index < 0 || index >= imageList.length) return; + + const imgElement = imageList[index]; + const parentImageDiv = imgElement.closest('.image'); + const fullImageUrl = imgElement.getAttribute('data-full'); // Use data-full for the full image URL + const title = imgElement.alt || ''; + const sourceUrl = parentImageDiv.querySelector('.img_source').href || '#'; // Source webpage URL + + const viewerImage = document.getElementById('viewer-image'); + const viewerTitle = document.getElementById('viewer-title'); + const viewerSourceButton = document.getElementById('viewer-source-button'); + const fullSizeLink = document.getElementById('viewer-full-size-link'); + const proxySizeLink = document.getElementById('viewer-proxy-size-link'); + const viewerImageLink = document.getElementById('viewer-image-link'); + + // Set the viewer image to the full image URL + viewerImage.src = fullImageUrl; + viewerTitle.textContent = title; + viewerSourceButton.href = sourceUrl; + fullSizeLink.href = sourceUrl; // Link to the source website + proxySizeLink.href = fullImageUrl; // Link to the proxied full-size image + viewerImageLink.href = fullImageUrl; // Make image clickable to open in new tab + } + // Attach event listener to the document body document.body.addEventListener('click', function(e) { let target = e.target; @@ -80,10 +96,30 @@ document.addEventListener('DOMContentLoaded', function() { imageView.classList.add('image_hide'); viewerOverlay.style.display = 'none'; viewerOpen = false; + currentIndex = -1; } - // Close viewer on overlay or button click + // Navigation functions + function showPreviousImage() { + if (currentIndex > 0) { + currentIndex--; + displayImage(currentIndex); + } + } + + function showNextImage() { + if (currentIndex < imageList.length - 1) { + currentIndex++; + displayImage(currentIndex); + } + } + + // Event listeners for navigation and closing document.getElementById('viewer-close-button').addEventListener('click', closeImageViewer); + document.getElementById('viewer-prev-button').addEventListener('click', showPreviousImage); + document.getElementById('viewer-next-button').addEventListener('click', showNextImage); + + // Close viewer when clicking outside the image viewerOverlay.addEventListener('click', function(e) { if (e.target === viewerOverlay) { closeImageViewer(); @@ -95,6 +131,10 @@ document.addEventListener('DOMContentLoaded', function() { if (viewerOverlay.style.display === 'flex') { if (e.key === 'Escape') { closeImageViewer(); + } else if (e.key === 'ArrowLeft') { + showPreviousImage(); + } else if (e.key === 'ArrowRight') { + showNextImage(); } } }); diff --git a/templates/images.html b/templates/images.html index 2616498..e27f839 100755 --- a/templates/images.html +++ b/templates/images.html @@ -153,9 +153,9 @@ - {{ if not .JsDisabled }} - - {{ end }} +
@@ -179,11 +179,13 @@ {{ range $index, $result := .Results }}
{{ if $.HardCacheEnabled }} - {{ if $.JsDisabled }} + + + +
{{ $result.Title }} - {{ end }} +
{{ else }} {{ $result.Title }} @@ -294,7 +296,7 @@ if (isFetching || noMoreImages) return; isFetching = true; page += 1; - + fetch(`/search?q=${encodeURIComponent(query)}&t=image&p=${page}&ajax=true`) .then(response => response.text()) .then(html => { @@ -302,13 +304,13 @@ let parser = new DOMParser(); let doc = parser.parseFromString(html, 'text/html'); let newImages = doc.querySelectorAll('.image'); - + if (newImages.length > 0) { let resultsContainer = document.querySelector('.images'); newImages.forEach(imageDiv => { // Append new images to the container resultsContainer.appendChild(imageDiv); - + // Get the img element let img = imageDiv.querySelector('img'); if (img) { @@ -318,10 +320,12 @@ img.onerror = function() { handleImageError(img); }; - + let id = img.getAttribute('data-id'); - imageElements.push(img); - imageIds.push(id); + if (id) { // Only include if ID is not empty + imageElements.push(img); + imageIds.push(id); + } } } }); @@ -381,7 +385,9 @@ // Initialize imageElements and imageIds if (hardCacheEnabled) { imageElements = Array.from(document.querySelectorAll('img[data-id]')); - imageIds = imageElements.map(img => img.getAttribute('data-id')); + imageIds = imageElements + .map(img => img.getAttribute('data-id')) + .filter(id => id); // Exclude empty IDs // Replace images with placeholders imageElements.forEach(img => { @@ -409,20 +415,6 @@ // Remove 'js-enabled' class from content document.getElementById('content').classList.remove('js-enabled'); })(); - - - - - \ No newline at end of file diff --git a/templates/images_only.html b/templates/images_only.html index 09774d5..765045b 100644 --- a/templates/images_only.html +++ b/templates/images_only.html @@ -1,4 +1,3 @@ - {{ range $index, $result := .Results }}
{{ end }} {{ translate "site_description" }} + + + @@ -17,9 +31,14 @@ - - + +
+ +
+
+ menu +
@@ -140,38 +159,48 @@
- - -
- -

{{ translate "web" }}

+
+ +
+ +
+ +
+ +
+ +
+ +
+
-
- -

{{ translate "images" }}

+
+ +
+ +
+
- -
- -

{{ translate "videos" }}

-
- -
- -

{{ translate "forums" }}

-
- -
- -

{{ translate "maps" }}

-
- -
- -

{{ translate "torrents" }}

-
-
+
@@ -180,9 +209,9 @@ \ No newline at end of file