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 ( var (
cachingImages = make(map[string]*sync.Mutex) cachingImages = make(map[string]*sync.Mutex)
cachingImagesMu 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{}) invalidImageIDs = make(map[string]struct{})
invalidImageIDsMu sync.Mutex 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" 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) cachedImagePath := filepath.Join(cacheDir, filename)
tempImagePath := cachedImagePath + ".tmp" tempImagePath := cachedImagePath + ".tmp"
@ -181,46 +198,105 @@ func cacheImage(imageURL, filename, imageID string) (string, bool, error) {
return cachedImagePath, true, nil 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) 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" 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)
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"
}
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("Content-Type", contentType)
w.Header().Set("Cache-Control", "public, max-age=31536000") // Cache the image for 1 year w.Header().Set("Cache-Control", "public, max-age=31536000")
http.ServeFile(w, r, cachedImagePath) 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
}
// 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
}
// 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) { func handleImageStatus(w http.ResponseWriter, r *http.Request) {
@ -228,45 +304,54 @@ func handleImageStatus(w http.ResponseWriter, r *http.Request) {
ids := strings.Split(imageIDs, ",") ids := strings.Split(imageIDs, ",")
statusMap := make(map[string]string) 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 { for _, id := range ids {
// Check if the image ID is in the invalidImageIDs map if id == "" {
if _, invalid := invalidImageIDs[id]; invalid {
// Image is invalid, set status to "missing"
statusMap[id] = "/static/images/missing.svg"
continue continue
} }
// Check for different possible extensions // Check for cached full or thumbnail images
extensions := []string{".webp", ".svg"} cacheDir := "image_cache"
var cachedImagePath string extensions := []string{"webp", "svg"} // Extensions without leading dots
var found bool imageReady := false
// Check thumbnail first
for _, ext := range extensions { for _, ext := range extensions {
filename := id + ext thumbFilename := fmt.Sprintf("%s_thumb.%s", id, ext)
path := filepath.Join(cacheDir, filename) thumbPath := filepath.Join(cacheDir, thumbFilename)
if _, err := os.Stat(path); err == nil {
cachedImagePath = "/image_cache/" + filename if _, err := os.Stat(thumbPath); err == nil {
found = true statusMap[id] = fmt.Sprintf("/image/%s_thumb.%s", id, ext)
imageReady = true
break break
} }
} }
if found { // If no thumbnail, check full image
statusMap[id] = cachedImagePath if !imageReady {
} else { for _, ext := range extensions {
// Image is not ready yet fullFilename := fmt.Sprintf("%s_full.%s", id, ext)
statusMap[id] = "" 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
}
} }
} }
printDebug("Status map: %v", statusMap) // 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
}
}
}
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(statusMap) json.NewEncoder(w).Encode(statusMap)
@ -338,3 +423,20 @@ func removeImageResultFromCache(query string, page int, safe bool, lang string,
delete(rc.results, keyStr) 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" "strings"
) )
func handleImageProxy(w http.ResponseWriter, r *http.Request) { func serveImageProxy(w http.ResponseWriter, imageURL string) {
// 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
}
// Fetch the image from the external URL // Fetch the image from the external URL
resp, err := http.Get(imageURL) resp, err := http.Get(imageURL)
if err != nil { if err != nil {
printWarn("Error fetching image: %v", err) printWarn("Error fetching image: %v", err)
serveMissingImage(w, r) serveMissingImage(w, nil)
return return
} }
defer resp.Body.Close() defer resp.Body.Close()
// Check if the request was successful // Check if the request was successful
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
serveMissingImage(w, r) serveMissingImage(w, nil)
return return
} }
@ -35,7 +28,7 @@ func handleImageProxy(w http.ResponseWriter, r *http.Request) {
if contentType != "" && strings.HasPrefix(contentType, "image/") { if contentType != "" && strings.HasPrefix(contentType, "image/") {
w.Header().Set("Content-Type", contentType) w.Header().Set("Content-Type", contentType)
} else { } else {
serveMissingImage(w, r) serveMissingImage(w, nil)
return return
} }

View file

@ -5,7 +5,6 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
"strconv"
"strings" "strings"
"time" "time"
@ -39,27 +38,13 @@ func PerformBingImageSearch(query, safe, lang string, page int) ([]ImageSearchRe
// Extract data using goquery // Extract data using goquery
var results []ImageSearchResult var results []ImageSearchResult
doc.Find(".iusc").Each(func(i int, s *goquery.Selection) { 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) // Extract the m parameter (JSON-encoded image metadata)
metadata, exists := s.Attr("m") metadata, exists := s.Attr("m")
if !exists { if !exists {
return 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{} var data map[string]interface{}
if err := json.Unmarshal([]byte(metadata), &data); err == nil { if err := json.Unmarshal([]byte(metadata), &data); err == nil {
mediaURL, ok := data["murl"].(string) mediaURL, ok := data["murl"].(string)
@ -67,19 +52,43 @@ func PerformBingImageSearch(query, safe, lang string, page int) ([]ImageSearchRe
return 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 // Extract title from the metadata
title, _ := data["t"].(string) title, _ := data["t"].(string)
// Apply the image proxy // Extract dimensions if available
proxiedFullURL := "/imgproxy?url=" + mediaURL width := 0
proxiedThumbURL := "/imgproxy?url=" + imgSrc 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{ results = append(results, ImageSearchResult{
Thumb: imgSrc, Thumb: thumbURL,
Title: strings.TrimSpace(title), Title: strings.TrimSpace(title),
Full: imgSrc, Full: directImageURL,
Source: mediaURL, Source: mediaURL,
ProxyFull: proxiedFullURL, // Proxied full-size image URL
ProxyThumb: proxiedThumbURL, // Proxied thumbnail URL
Width: width, Width: width,
Height: height, Height: height,
}) })

View file

@ -156,8 +156,6 @@ func PerformDeviantArtImageSearch(query, safe, lang string, page int) ([]ImageSe
Width: 0, Width: 0,
Height: 0, Height: 0,
Source: resultURL, Source: resultURL,
ProxyThumb: "/imgproxy?url=" + imgSrc, // Proxied thumbnail
ProxyFull: "/imgproxy?url=" + imgSrc, // Proxied full-size image
} }
} }
}(imgSrc, resultURL, title) }(imgSrc, resultURL, title)

View file

@ -64,10 +64,6 @@ func PerformImgurImageSearch(query, safe, lang string, page int) ([]ImageSearchR
width, _ := strconv.Atoi(s.Find("a img").AttrOr("width", "0")) width, _ := strconv.Atoi(s.Find("a img").AttrOr("width", "0"))
height, _ := strconv.Atoi(s.Find("a img").AttrOr("height", "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{ results = append(results, ImageSearchResult{
Thumb: thumbnailSrc, Thumb: thumbnailSrc,
Title: strings.TrimSpace(title), Title: strings.TrimSpace(title),
@ -75,8 +71,6 @@ func PerformImgurImageSearch(query, safe, lang string, page int) ([]ImageSearchR
Width: width, Width: width,
Height: height, Height: height,
Source: "https://imgur.com" + urlPath, Source: "https://imgur.com" + urlPath,
ProxyFull: proxyFullURL,
ProxyThumb: proxyThumbURL,
}) })
}) })

View file

@ -163,8 +163,6 @@ func PerformQwantImageSearch(query, safe, lang string, page int) ([]ImageSearchR
Title: item.Title, Title: item.Title,
Full: item.Media, Full: item.Media,
Source: item.Url, Source: item.Url,
ProxyFull: "/imgproxy?url=" + item.Media,
ProxyThumb: "/imgproxy?url=" + item.Media,
Width: item.Width, Width: item.Width,
Height: item.Height, Height: item.Height,
} }

139
images.go
View file

@ -58,6 +58,12 @@ func handleImageSearch(w http.ResponseWriter, r *http.Request, settings UserSett
"JsDisabled": jsDisabled, "JsDisabled": jsDisabled,
} }
if r.URL.Query().Get("ajax") == "true" {
// Render only the images
renderTemplate(w, "images_only.html", data)
return
}
// Render the full page // Render the full page
renderTemplate(w, "images.html", data) 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 { func fetchImageResults(query, safe, lang string, page int, synchronous bool) []ImageSearchResult {
var results []ImageSearchResult var results []ImageSearchResult
engineCount := len(imageSearchEngines) engineCount := len(imageSearchEngines)
safeBool := safe == "active"
// Determine the engine to use based on the page number // Determine the engine to use based on the page number
engineIndex := (page - 1) % engineCount engineIndex := (page - 1) % engineCount
@ -120,37 +125,47 @@ func fetchImageResults(query, safe, lang string, page int, synchronous bool) []I
} else { } else {
for _, result := range searchResults { for _, result := range searchResults {
imageResult := result.(ImageSearchResult) imageResult := result.(ImageSearchResult)
if config.HardCacheEnabled {
// 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 // Generate hash and set up caching
hasher := md5.New() hasher := md5.New()
hasher.Write([]byte(imageResult.Full)) hasher.Write([]byte(imageResult.Full))
hash := hex.EncodeToString(hasher.Sum(nil)) hash := hex.EncodeToString(hasher.Sum(nil))
filename := hash + ".webp"
imageResult.ID = hash imageResult.ID = hash
imageResult.ProxyFull = "/image_cache/" + filename
if synchronous { // Store mapping from imageID_full and imageID_thumb to URLs
// Synchronously cache the image imageURLMapMu.Lock()
_, success, err := cacheImage(imageResult.Full, filename, imageResult.ID) 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 { if err != nil || !success {
printWarn("Failed to cache image %s: %v", imageResult.Full, err) printWarn("Failed to cache thumbnail image %s: %v", imgResult.Thumb, err)
// Fallback to proxy URL removeImageResultFromCache(query, page, safe == "active", lang, imgResult.ID)
imageResult.ProxyFull = "/imgproxy?url=" + imageResult.Full
} }
}(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 { } else {
// Start caching and validation in the background // Hard cache disabled, proxy both thumb and full images
go func(imgResult ImageSearchResult, originalURL, filename string) { imageResult.ProxyThumb = fmt.Sprintf("/image/%s_thumb", hash)
_, success, err := cacheImage(originalURL, filename, imgResult.ID) imageResult.ProxyFull = fmt.Sprintf("/image/%s_full", hash)
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
} }
results = append(results, imageResult) results = append(results, imageResult)
} }
} }
@ -170,44 +185,46 @@ func fetchImageResults(query, safe, lang string, page int, synchronous bool) []I
} }
for _, result := range searchResults { for _, result := range searchResults {
imageResult := result.(ImageSearchResult) imageResult := result.(ImageSearchResult)
if config.HardCacheEnabled {
// 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 // Generate hash and set up caching
hasher := md5.New() hasher := md5.New()
hasher.Write([]byte(imageResult.Full)) hasher.Write([]byte(imageResult.Full))
hash := hex.EncodeToString(hasher.Sum(nil)) hash := hex.EncodeToString(hasher.Sum(nil))
filename := hash + ".webp"
imageResult.ID = hash imageResult.ID = hash
imageResult.ProxyFull = "/image_cache/" + filename
if synchronous { // Store mapping from imageID_full and imageID_thumb to URLs
// Synchronously cache the image imageURLMapMu.Lock()
_, success, err := cacheImage(imageResult.Full, filename, imageResult.ID) imageURLMap[fmt.Sprintf("%s_full", hash)] = imageResult.Full
if err != nil { imageURLMap[fmt.Sprintf("%s_thumb", hash)] = imageResult.Thumb
printWarn("Failed to cache image %s: %v", imageResult.Full, err) imageURLMapMu.Unlock()
// Skip this image
continue if config.HardCacheEnabled {
} // Cache the thumbnail image asynchronously
if !success { go func(imgResult ImageSearchResult) {
// Skip this image _, success, err := cacheImage(imgResult.Thumb, imgResult.ID, true)
continue 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 { } else {
// Start caching and validation in the background // Hard cache disabled, proxy both thumb and full images
go func(imgResult ImageSearchResult, originalURL, filename string) { imageResult.ProxyThumb = fmt.Sprintf("/image/%s_thumb", hash)
_, success, err := cacheImage(originalURL, filename, imgResult.ID) imageResult.ProxyFull = fmt.Sprintf("/image/%s_full", hash)
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
} }
results = append(results, imageResult) 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 // // Filter out images that failed to cache or are invalid
validResults := make([]ImageSearchResult, 0, len(results)) // validResults := make([]ImageSearchResult, 0, len(results))
for _, imageResult := range results { // for _, imageResult := range results {
if imageResult.ProxyFull != "" { // if imageResult.ProxyFull != "" {
validResults = append(validResults, imageResult) // validResults = append(validResults, imageResult)
} else { // } else {
printWarn("Skipping invalid image with ID %s", imageResult.ID) // printWarn("Skipping invalid image with ID %s", imageResult.ID)
} // }
} // }
// Final debug print to show the count of results fetched // Final debug print to show the count of results fetched
printInfo("Fetched %d image results for overall page %d", len(results), page) 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) { 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("/", handleSearch)
http.HandleFunc("/search", handleSearch) http.HandleFunc("/search", handleSearch)
http.HandleFunc("/suggestions", handleSuggestions) 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("/node", handleNodeRequest)
http.HandleFunc("/settings", handleSettings) http.HandleFunc("/settings", handleSettings)
http.HandleFunc("/save-settings", handleSaveSettings) http.HandleFunc("/save-settings", handleSaveSettings)
http.HandleFunc("/image_cache/", handleCachedImages) http.HandleFunc("/image/", handleImageServe)
http.HandleFunc("/image_status", handleImageStatus) http.HandleFunc("/image_status", handleImageStatus)
http.HandleFunc("/opensearch.xml", func(w http.ResponseWriter, r *http.Request) { http.HandleFunc("/opensearch.xml", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/opensearchdescription+xml") w.Header().Set("Content-Type", "application/opensearchdescription+xml")

View file

@ -44,8 +44,9 @@ body, html {
color: var(--text-color); color: var(--text-color);
} }
body { button, p {
visibility: hidden; font-family: 'Inter', Arial, Helvetica, sans-serif;
font-weight: 400;
} }
body.menu-open { body.menu-open {
@ -259,6 +260,7 @@ body.menu-open {
} }
.settings-icon-link-search:hover { .settings-icon-link-search:hover {
text-decoration: none;
color: var(--blue); color: var(--blue);
transition: color 0.3s ease; transition: color 0.3s ease;
} }

View file

@ -1,21 +1,28 @@
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
let viewerOpen = false; 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'); const viewerOverlay = document.getElementById('image-viewer-overlay');
viewerOverlay.innerHTML = ` viewerOverlay.innerHTML = `
<div id="image-viewer" class="image_view image_hide"> <div id="image-viewer" class="image_view image_hide">
<div class="image-view-close"> <div class="image-view-close">
<!-- <button class="btn-nostyle"> <button class="btn-nostyle" id="viewer-prev-button">
<div id="viewer-prev-button" class="material-icons-round icon_visibility clickable image-before">navigate_before</div> <div class="material-icons-round icon_visibility clickable image-before">navigate_before</div>
</button> </button>
<button class="btn-nostyle"> <button class="btn-nostyle" id="viewer-next-button">
<div id="viewer-next-button" class="material-icons-round icon_visibility clickable image-next">navigate_next</div> <div class="material-icons-round icon_visibility clickable image-next">navigate_next</div>
</button> FIX THIS LATER! --!> </button>
<button class="btn-nostyle"> <button class="btn-nostyle" id="viewer-close-button">
<div id="viewer-close-button" class="material-icons-round icon_visibility clickable image-close">close</div> <div class="material-icons-round icon_visibility clickable image-close">close</div>
</button> </button>
</div> </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"> <div class="view-image" id="viewer-image-container">
<img id="viewer-image" class="view-image-img" src="" alt=""> <img id="viewer-image" class="view-image-img" src="" alt="">
</div> </div>
@ -23,8 +30,8 @@ document.addEventListener('DOMContentLoaded', function() {
<p class="image-alt" id="viewer-title"></p> <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>View source: <a id="viewer-source-button" class="image-source" href="" target="_blank"></a></p>
<p> <p>
<a class="full-size" href="#">Show source website</a> <a class="full-size" id="viewer-full-size-link" href="#" target="_blank">Show source website</a>
<a class="proxy-size" href="#">Show in fullscreen</a> <a class="proxy-size" id="viewer-proxy-size-link" href="#" target="_blank">Show in fullscreen</a>
</p> </p>
</div> </div>
`; `;
@ -32,36 +39,45 @@ document.addEventListener('DOMContentLoaded', function() {
const imageView = document.getElementById('image-viewer'); const imageView = document.getElementById('image-viewer');
function openImageViewer(element) { function openImageViewer(element) {
initializeImageList(); // Update the image list
const parentImageDiv = element.closest('.image'); const parentImageDiv = element.closest('.image');
if (!parentImageDiv) return; if (!parentImageDiv) return;
const imgElement = parentImageDiv.querySelector('img.clickable'); currentIndex = imageList.findIndex(img => img === parentImageDiv.querySelector('img.clickable'));
const fullImageUrl = imgElement.dataset.proxyFull; // Use data-proxy-full for ProxyFull if (currentIndex === -1) return;
const thumbnailUrl = imgElement.src; // Use ProxyThumb for the thumbnail
const title = imgElement.alt;
const sourceUrl = parentImageDiv.querySelector('.img_source').href; // Source webpage URL
if (!fullImageUrl || viewerOpen) { displayImage(currentIndex);
return; // Don't open if data is missing or viewer is already open
}
viewerOpen = true; viewerOpen = true;
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 viewerImage = document.getElementById('viewer-image');
const viewerTitle = document.getElementById('viewer-title'); const viewerTitle = document.getElementById('viewer-title');
const viewerSourceButton = document.getElementById('viewer-source-button'); const viewerSourceButton = document.getElementById('viewer-source-button');
const fullSizeLink = imageView.querySelector('.full-size'); const fullSizeLink = document.getElementById('viewer-full-size-link');
const proxySizeLink = imageView.querySelector('.proxy-size'); const proxySizeLink = document.getElementById('viewer-proxy-size-link');
const viewerImageLink = document.getElementById('viewer-image-link');
// Set the viewer image to ProxyFull // Set the viewer image to the full image URL
viewerImage.src = fullImageUrl; viewerImage.src = fullImageUrl;
viewerTitle.textContent = title; viewerTitle.textContent = title;
viewerSourceButton.href = sourceUrl; viewerSourceButton.href = sourceUrl;
fullSizeLink.href = sourceUrl; // Link to the source website fullSizeLink.href = sourceUrl; // Link to the source website
proxySizeLink.href = fullImageUrl; // Link to the proxied full-size image proxySizeLink.href = fullImageUrl; // Link to the proxied full-size image
viewerImageLink.href = fullImageUrl; // Make image clickable to open in new tab
viewerOverlay.style.display = 'flex';
imageView.classList.remove('image_hide');
imageView.classList.add('image_show');
} }
// Attach event listener to the document body // Attach event listener to the document body
@ -80,10 +96,30 @@ document.addEventListener('DOMContentLoaded', function() {
imageView.classList.add('image_hide'); imageView.classList.add('image_hide');
viewerOverlay.style.display = 'none'; viewerOverlay.style.display = 'none';
viewerOpen = false; 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-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) { viewerOverlay.addEventListener('click', function(e) {
if (e.target === viewerOverlay) { if (e.target === viewerOverlay) {
closeImageViewer(); closeImageViewer();
@ -95,6 +131,10 @@ document.addEventListener('DOMContentLoaded', function() {
if (viewerOverlay.style.display === 'flex') { if (viewerOverlay.style.display === 'flex') {
if (e.key === 'Escape') { if (e.key === 'Escape') {
closeImageViewer(); 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> <button name="t" value="file" class="clickable">{{ translate "torrents" }}</button>
</div> </div>
</div> </div>
{{ if not .JsDisabled }} <noscript>
<input type="hidden" name="js_enabled" value="true"> <input type="hidden" name="js_enabled" value="true">
{{ end }} </noscript>
</form> </form>
<form class="results_settings" action="/search" method="get"> <form class="results_settings" action="/search" method="get">
<input type="hidden" name="q" value="{{ .Query }}"> <input type="hidden" name="q" value="{{ .Query }}">
@ -179,11 +179,13 @@
{{ range $index, $result := .Results }} {{ range $index, $result := .Results }}
<div class="image"> <div class="image">
{{ if $.HardCacheEnabled }} {{ if $.HardCacheEnabled }}
{{ if $.JsDisabled }} <noscript>
<!-- JavaScript is disabled; serve actual images --> <!-- JavaScript is disabled; serve actual images -->
<img src="{{ $result.ProxyFull }}" alt="{{ $result.Title }}" class="clickable" /> <img src="{{ $result.ProxyFull }}" alt="{{ $result.Title }}" class="clickable" />
{{ else }} </noscript>
<!-- JavaScript is enabled; use placeholders --> <!-- JavaScript is enabled; use placeholders -->
<div id="content" class="js-enabled">
<img <img
src="/static/images/placeholder.svg" src="/static/images/placeholder.svg"
data-id="{{ $result.ID }}" data-id="{{ $result.ID }}"
@ -192,7 +194,7 @@
alt="{{ $result.Title }}" alt="{{ $result.Title }}"
class="clickable" class="clickable"
/> />
{{ end }} </div>
{{ else }} {{ else }}
<!-- HardCacheEnabled is false; serve images directly --> <!-- HardCacheEnabled is false; serve images directly -->
<img src="{{ $result.ProxyFull }}" alt="{{ $result.Title }}" class="clickable" /> <img src="{{ $result.ProxyFull }}" alt="{{ $result.Title }}" class="clickable" />
@ -320,10 +322,12 @@
}; };
let id = img.getAttribute('data-id'); let id = img.getAttribute('data-id');
if (id) { // Only include if ID is not empty
imageElements.push(img); imageElements.push(img);
imageIds.push(id); imageIds.push(id);
} }
} }
}
}); });
if (hardCacheEnabled) { if (hardCacheEnabled) {
checkImageStatus(); checkImageStatus();
@ -381,7 +385,9 @@
// Initialize imageElements and imageIds // Initialize imageElements and imageIds
if (hardCacheEnabled) { if (hardCacheEnabled) {
imageElements = Array.from(document.querySelectorAll('img[data-id]')); 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 // Replace images with placeholders
imageElements.forEach(img => { imageElements.forEach(img => {
@ -410,19 +416,5 @@
document.getElementById('content').classList.remove('js-enabled'); document.getElementById('content').classList.remove('js-enabled');
})(); })();
</script> </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> </body>
</html> </html>

View file

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

View file

@ -7,6 +7,20 @@
<meta name="darkreader-lock"> <meta name="darkreader-lock">
{{ end }} {{ end }}
<title>{{ translate "site_description" }}</title> <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/style-search.css">
<link rel="stylesheet" href="/static/css/{{.Theme}}.css"> <link rel="stylesheet" href="/static/css/{{.Theme}}.css">
<link rel="search" type="application/opensearchdescription+xml" title="{{ translate "site_name" }}" href="/opensearch.xml"> <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 }}"> <link rel="apple-touch-icon" href="{{ .IconPathPNG }}">
</head> </head>
<body> <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 --> <!-- Side Navigation Menu -->
<div id="mySidenav" class="side-nav"> <div id="mySidenav" class="side-nav">
@ -140,36 +159,46 @@
</div> </div>
</div> </div>
<div class="search-type-icons"> <div class="search-type-icons">
<input type="hidden" name="p" value="1"> <div class="icon-button">
<button id="sub-search-wrapper-ico-text" class="material-icons-round clickable" name="t" value="text">
<div class="icon-button" onclick="document.getElementById('sub-search-wrapper-ico-text').click()"> <span>search</span>
<button id="sub-search-wrapper-ico-text" class="material-icons-round clickable" name="t" value="text">search</button>
<p>{{ translate "web" }}</p> <p>{{ translate "web" }}</p>
</button>
</div> </div>
<div class="icon-button" onclick="document.getElementById('sub-search-wrapper-ico-image').click()"> <div class="icon-button">
<button id="sub-search-wrapper-ico-image" class="material-icons-round clickable" name="t" value="image">image</button> <button id="sub-search-wrapper-ico-image" class="material-icons-round clickable" name="t" value="image">
<span>image</span>
<p>{{ translate "images" }}</p> <p>{{ translate "images" }}</p>
</button>
</div> </div>
<div class="icon-button" onclick="document.getElementById('sub-search-wrapper-ico-video').click()"> <div class="icon-button">
<button id="sub-search-wrapper-ico-video" class="material-icons-round clickable" name="t" value="video">movie</button> <button id="sub-search-wrapper-ico-video" class="material-icons-round clickable" name="t" value="video">
<span>movie</span>
<p>{{ translate "videos" }}</p> <p>{{ translate "videos" }}</p>
</button>
</div> </div>
<div class="icon-button" onclick="document.getElementById('sub-search-wrapper-ico-forum').click()"> <div class="icon-button">
<button id="sub-search-wrapper-ico-forum" class="material-icons-round clickable" name="t" value="forum">forum</button> <button id="sub-search-wrapper-ico-forum" class="material-icons-round clickable" name="t" value="forum">
<span>forum</span>
<p>{{ translate "forums" }}</p> <p>{{ translate "forums" }}</p>
</button>
</div> </div>
<div class="icon-button" onclick="document.getElementById('sub-search-wrapper-ico-map').click()"> <div class="icon-button">
<button id="sub-search-wrapper-ico-map" class="material-icons-round clickable" name="t" value="map">map</button> <button id="sub-search-wrapper-ico-map" class="material-icons-round clickable" name="t" value="map">
<span>map</span>
<p>{{ translate "maps" }}</p> <p>{{ translate "maps" }}</p>
</button>
</div> </div>
<div class="icon-button" onclick="document.getElementById('sub-search-wrapper-ico-file').click()"> <div class="icon-button">
<button id="sub-search-wrapper-ico-file" class="material-icons-round clickable" name="t" value="file">share</button> <button id="sub-search-wrapper-ico-file" class="material-icons-round clickable" name="t" value="file">
<span>share</span>
<p>{{ translate "torrents" }}</p> <p>{{ translate "torrents" }}</p>
</button>
</div> </div>
</div> </div>
</div> </div>
@ -180,9 +209,9 @@
<script defer src="/static/js/sidemenu.js"></script> <script defer src="/static/js/sidemenu.js"></script>
<script defer src="/static/js/autocomplete.js"></script> <script defer src="/static/js/autocomplete.js"></script>
<script> <script>
window.addEventListener('load', function() { // When JS is detected, update the DOM
document.body.style.visibility = 'visible'; document.getElementById('js-enabled').style.display = 'block';
}); document.getElementById('js-disabled').style.display = 'none';
</script> </script>
</body> </body>
</html> </html>