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 = `
- Show source website - Show in fullscreen + Show source website + Show in fullscreen