code cleanup & fixed compatibility for non-JS users & fixed fullscreen images being in low resolution

This commit is contained in:
partisan 2024-11-19 10:36:33 +01:00
parent 0d083f53e7
commit db89f9c781
13 changed files with 474 additions and 300 deletions

View file

@ -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"
}
}

View file

@ -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
}

View file

@ -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,
})
}
})

View file

@ -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)

View file

@ -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,
})
})

View file

@ -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)
}

169
images.go
View file

@ -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) {

View file

@ -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")

View file

@ -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;
}

View file

@ -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 = `
<div id="image-viewer" class="image_view image_hide">
<div class="image-view-close">
<!-- <button class="btn-nostyle">
<div id="viewer-prev-button" class="material-icons-round icon_visibility clickable image-before">navigate_before</div>
<button class="btn-nostyle" id="viewer-prev-button">
<div class="material-icons-round icon_visibility clickable image-before">navigate_before</div>
</button>
<button class="btn-nostyle">
<div id="viewer-next-button" class="material-icons-round icon_visibility clickable image-next">navigate_next</div>
</button> FIX THIS LATER! --!>
<button class="btn-nostyle">
<div id="viewer-close-button" class="material-icons-round icon_visibility clickable image-close">close</div>
<button class="btn-nostyle" id="viewer-next-button">
<div class="material-icons-round icon_visibility clickable image-next">navigate_next</div>
</button>
<button class="btn-nostyle" id="viewer-close-button">
<div class="material-icons-round icon_visibility clickable image-close">close</div>
</button>
</div>
<a class="image-viewer-link">
<a class="image-viewer-link" id="viewer-image-link" target="_blank">
<div class="view-image" id="viewer-image-container">
<img id="viewer-image" class="view-image-img" src="" alt="">
</div>
@ -23,8 +30,8 @@ document.addEventListener('DOMContentLoaded', function() {
<p class="image-alt" id="viewer-title"></p>
<p>View source: <a id="viewer-source-button" class="image-source" href="" target="_blank"></a></p>
<p>
<a class="full-size" href="#">Show source website</a>
<a class="proxy-size" href="#">Show in fullscreen</a>
<a class="full-size" id="viewer-full-size-link" href="#" target="_blank">Show source website</a>
<a class="proxy-size" id="viewer-proxy-size-link" href="#" target="_blank">Show in fullscreen</a>
</p>
</div>
`;
@ -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();
}
}
});

View file

@ -153,9 +153,9 @@
<button name="t" value="file" class="clickable">{{ translate "torrents" }}</button>
</div>
</div>
{{ if not .JsDisabled }}
<input type="hidden" name="js_enabled" value="true">
{{ end }}
<noscript>
<input type="hidden" name="js_enabled" value="true">
</noscript>
</form>
<form class="results_settings" action="/search" method="get">
<input type="hidden" name="q" value="{{ .Query }}">
@ -179,11 +179,13 @@
{{ range $index, $result := .Results }}
<div class="image">
{{ if $.HardCacheEnabled }}
{{ if $.JsDisabled }}
<noscript>
<!-- JavaScript is disabled; serve actual images -->
<img src="{{ $result.ProxyFull }}" alt="{{ $result.Title }}" class="clickable" />
{{ else }}
<!-- JavaScript is enabled; use placeholders -->
</noscript>
<!-- JavaScript is enabled; use placeholders -->
<div id="content" class="js-enabled">
<img
src="/static/images/placeholder.svg"
data-id="{{ $result.ID }}"
@ -192,7 +194,7 @@
alt="{{ $result.Title }}"
class="clickable"
/>
{{ end }}
</div>
{{ else }}
<!-- HardCacheEnabled is false; serve images directly -->
<img src="{{ $result.ProxyFull }}" alt="{{ $result.Title }}" class="clickable" />
@ -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');
})();
</script>
<script type="text/javascript">
// Check if 'js_enabled' is not present in the URL
if (!window.location.search.includes('js_enabled=true')) {
// Redirect to the same URL with 'js_enabled=true'
var separator = window.location.search.length ? '&' : '?';
window.location.href = window.location.href + separator + 'js_enabled=true';
}
</script>
<script>
// Check if JavaScript is enabled and modify the DOM accordingly
document.getElementById('content').classList.remove('js-enabled');
</script>
</body>
</html>

View file

@ -1,4 +1,3 @@
<!-- Images Grid -->
{{ range $index, $result := .Results }}
<div class="image">
<img

View file

@ -7,6 +7,20 @@
<meta name="darkreader-lock">
{{ end }}
<title>{{ translate "site_description" }}</title>
<!-- Inline Style to Avoid Flashbang -->
<style>
body {
background-color: {{ if .IsThemeDark }} #121212 {{ else }} #ffffff {{ end }};
color: {{ if .IsThemeDark }} #ffffff {{ else }} #000000 {{ end }};
}
#js-enabled {
display: none;
}
#js-disabled {
display: block;
}
</style>
<link rel="stylesheet" href="/static/css/style-search.css">
<link rel="stylesheet" href="/static/css/{{.Theme}}.css">
<link rel="search" type="application/opensearchdescription+xml" title="{{ translate "site_name" }}" href="/opensearch.xml">
@ -17,9 +31,14 @@
<link rel="apple-touch-icon" href="{{ .IconPathPNG }}">
</head>
<body>
<!-- Menu Button -->
<button class="material-icons-round settings-icon-link-search" onclick="openNav()">menu</button>
<!-- Menu Button -->
<div id="js-enabled">
<button class="material-icons-round settings-icon-link-search" onclick="openNav()">menu</button>
</div>
<div id="js-disabled">
<a href="/settings" class="material-icons-round settings-icon-link-search">menu</a>
</div>
<!-- Side Navigation Menu -->
<div id="mySidenav" class="side-nav">
@ -140,38 +159,48 @@
</div>
</div>
<div class="search-type-icons">
<input type="hidden" name="p" value="1">
<div class="icon-button" onclick="document.getElementById('sub-search-wrapper-ico-text').click()">
<button id="sub-search-wrapper-ico-text" class="material-icons-round clickable" name="t" value="text">search</button>
<p>{{ translate "web" }}</p>
<div class="icon-button">
<button id="sub-search-wrapper-ico-text" class="material-icons-round clickable" name="t" value="text">
<span>search</span>
<p>{{ translate "web" }}</p>
</button>
</div>
<div class="icon-button">
<button id="sub-search-wrapper-ico-image" class="material-icons-round clickable" name="t" value="image">
<span>image</span>
<p>{{ translate "images" }}</p>
</button>
</div>
<div class="icon-button">
<button id="sub-search-wrapper-ico-video" class="material-icons-round clickable" name="t" value="video">
<span>movie</span>
<p>{{ translate "videos" }}</p>
</button>
</div>
<div class="icon-button">
<button id="sub-search-wrapper-ico-forum" class="material-icons-round clickable" name="t" value="forum">
<span>forum</span>
<p>{{ translate "forums" }}</p>
</button>
</div>
<div class="icon-button" onclick="document.getElementById('sub-search-wrapper-ico-image').click()">
<button id="sub-search-wrapper-ico-image" class="material-icons-round clickable" name="t" value="image">image</button>
<p>{{ translate "images" }}</p>
<div class="icon-button">
<button id="sub-search-wrapper-ico-map" class="material-icons-round clickable" name="t" value="map">
<span>map</span>
<p>{{ translate "maps" }}</p>
</button>
</div>
<div class="icon-button">
<button id="sub-search-wrapper-ico-file" class="material-icons-round clickable" name="t" value="file">
<span>share</span>
<p>{{ translate "torrents" }}</p>
</button>
</div>
<div class="icon-button" onclick="document.getElementById('sub-search-wrapper-ico-video').click()">
<button id="sub-search-wrapper-ico-video" class="material-icons-round clickable" name="t" value="video">movie</button>
<p>{{ translate "videos" }}</p>
</div>
<div class="icon-button" onclick="document.getElementById('sub-search-wrapper-ico-forum').click()">
<button id="sub-search-wrapper-ico-forum" class="material-icons-round clickable" name="t" value="forum">forum</button>
<p>{{ translate "forums" }}</p>
</div>
<div class="icon-button" onclick="document.getElementById('sub-search-wrapper-ico-map').click()">
<button id="sub-search-wrapper-ico-map" class="material-icons-round clickable" name="t" value="map">map</button>
<p>{{ translate "maps" }}</p>
</div>
<div class="icon-button" onclick="document.getElementById('sub-search-wrapper-ico-file').click()">
<button id="sub-search-wrapper-ico-file" class="material-icons-round clickable" name="t" value="file">share</button>
<p>{{ translate "torrents" }}</p>
</div>
</div>
</div>
</div>
</form>
@ -180,9 +209,9 @@
<script defer src="/static/js/sidemenu.js"></script>
<script defer src="/static/js/autocomplete.js"></script>
<script>
window.addEventListener('load', function() {
document.body.style.visibility = 'visible';
});
// When JS is detected, update the DOM
document.getElementById('js-enabled').style.display = 'block';
document.getElementById('js-disabled').style.display = 'none';
</script>
</body>
</html>