code cleanup & fixed compatibility for non-JS users & fixed fullscreen images being in low resolution
This commit is contained in:
parent
0d083f53e7
commit
db89f9c781
13 changed files with 474 additions and 300 deletions
222
cache-images.go
222
cache-images.go
|
@ -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) {
|
if hasExtension && imageType == "thumb" {
|
||||||
printDebug("Cached image not found: %s, serving missing.svg", cachedImagePath)
|
// Requesting cached thumbnail image
|
||||||
// Serve missing image
|
if _, err := os.Stat(cachedImagePath); err == nil {
|
||||||
missingImagePath := filepath.Join("static", "images", "missing.svg")
|
// Cached image exists, serve it
|
||||||
w.Header().Set("Content-Type", "image/svg+xml")
|
contentType := "image/webp"
|
||||||
http.ServeFile(w, r, missingImagePath)
|
w.Header().Set("Content-Type", contentType)
|
||||||
return
|
w.Header().Set("Cache-Control", "public, max-age=31536000")
|
||||||
} else if err != nil {
|
http.ServeFile(w, r, cachedImagePath)
|
||||||
printWarn("Error checking image file: %v", err)
|
return
|
||||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
} 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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine the content type based on the file extension
|
// For thumbnails, if HardCacheEnabled is true, and image not cached, serve missing image
|
||||||
extension := strings.ToLower(filepath.Ext(cachedImagePath))
|
if imageType == "thumb" && config.HardCacheEnabled {
|
||||||
var contentType string
|
// Thumbnail should be cached, but not found
|
||||||
switch extension {
|
serveMissingImage(w, r)
|
||||||
case ".svg":
|
return
|
||||||
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"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", contentType)
|
// For full images, proceed to proxy the image
|
||||||
w.Header().Set("Cache-Control", "public, max-age=31536000") // Cache the image for 1 year
|
|
||||||
http.ServeFile(w, r, cachedImagePath)
|
// 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,46 +304,55 @@ 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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")
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,21 +52,45 @@ 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
|
Width: width,
|
||||||
ProxyThumb: proxiedThumbURL, // Proxied thumbnail URL
|
Height: height,
|
||||||
Width: width,
|
|
||||||
Height: height,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -151,13 +151,11 @@ func PerformDeviantArtImageSearch(query, safe, lang string, page int) ([]ImageSe
|
||||||
// Verify if the image URL is accessible
|
// Verify if the image URL is accessible
|
||||||
if DeviantArtisValidImageURL(imgSrc, DeviantArtImageUserAgent, resultURL) {
|
if DeviantArtisValidImageURL(imgSrc, DeviantArtImageUserAgent, resultURL) {
|
||||||
resultsChan <- ImageSearchResult{
|
resultsChan <- ImageSearchResult{
|
||||||
Title: strings.TrimSpace(title),
|
Title: strings.TrimSpace(title),
|
||||||
Full: imgSrc,
|
Full: imgSrc,
|
||||||
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)
|
||||||
|
|
|
@ -64,19 +64,13 @@ 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),
|
||||||
Full: imgSrc,
|
Full: imgSrc,
|
||||||
Width: width,
|
Width: width,
|
||||||
Height: height,
|
Height: height,
|
||||||
Source: "https://imgur.com" + urlPath,
|
Source: "https://imgur.com" + urlPath,
|
||||||
ProxyFull: proxyFullURL,
|
|
||||||
ProxyThumb: proxyThumbURL,
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -159,14 +159,12 @@ func PerformQwantImageSearch(query, safe, lang string, page int) ([]ImageSearchR
|
||||||
|
|
||||||
// Populate the result
|
// Populate the result
|
||||||
results[i] = ImageSearchResult{
|
results[i] = ImageSearchResult{
|
||||||
Thumb: item.Media, // item.Thumbnail is not working
|
Thumb: item.Media, // item.Thumbnail is not working
|
||||||
Title: item.Title,
|
Title: item.Title,
|
||||||
Full: item.Media,
|
Full: item.Media,
|
||||||
Source: item.Url,
|
Source: item.Url,
|
||||||
ProxyFull: "/imgproxy?url=" + item.Media,
|
Width: item.Width,
|
||||||
ProxyThumb: "/imgproxy?url=" + item.Media,
|
Height: item.Height,
|
||||||
Width: item.Width,
|
|
||||||
Height: item.Height,
|
|
||||||
}
|
}
|
||||||
}(i, item)
|
}(i, item)
|
||||||
}
|
}
|
||||||
|
|
169
images.go
169
images.go
|
@ -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 {
|
|
||||||
// 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 {
|
// Skip image if thumbnail URL is empty
|
||||||
// Synchronously cache the image
|
if imageResult.Thumb == "" {
|
||||||
_, success, err := cacheImage(imageResult.Full, filename, imageResult.ID)
|
printWarn("Skipping image with empty thumbnail URL. Full URL: %s", imageResult.Full)
|
||||||
if err != nil || !success {
|
continue
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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)
|
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 {
|
|
||||||
// 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 {
|
// Skip image if thumbnail URL is empty
|
||||||
// Synchronously cache the image
|
if imageResult.Thumb == "" {
|
||||||
_, success, err := cacheImage(imageResult.Full, filename, imageResult.ID)
|
printWarn("Skipping image with empty thumbnail URL. Full URL: %s", imageResult.Full)
|
||||||
if err != nil {
|
continue
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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)
|
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) {
|
||||||
|
|
5
main.go
5
main.go
|
@ -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")
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,38 +39,47 @@ 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;
|
||||||
|
|
||||||
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';
|
viewerOverlay.style.display = 'flex';
|
||||||
imageView.classList.remove('image_hide');
|
imageView.classList.remove('image_hide');
|
||||||
imageView.classList.add('image_show');
|
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
|
// Attach event listener to the document body
|
||||||
document.body.addEventListener('click', function(e) {
|
document.body.addEventListener('click', function(e) {
|
||||||
let target = e.target;
|
let target = e.target;
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -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" />
|
||||||
|
@ -294,7 +296,7 @@
|
||||||
if (isFetching || noMoreImages) return;
|
if (isFetching || noMoreImages) return;
|
||||||
isFetching = true;
|
isFetching = true;
|
||||||
page += 1;
|
page += 1;
|
||||||
|
|
||||||
fetch(`/search?q=${encodeURIComponent(query)}&t=image&p=${page}&ajax=true`)
|
fetch(`/search?q=${encodeURIComponent(query)}&t=image&p=${page}&ajax=true`)
|
||||||
.then(response => response.text())
|
.then(response => response.text())
|
||||||
.then(html => {
|
.then(html => {
|
||||||
|
@ -302,13 +304,13 @@
|
||||||
let parser = new DOMParser();
|
let parser = new DOMParser();
|
||||||
let doc = parser.parseFromString(html, 'text/html');
|
let doc = parser.parseFromString(html, 'text/html');
|
||||||
let newImages = doc.querySelectorAll('.image');
|
let newImages = doc.querySelectorAll('.image');
|
||||||
|
|
||||||
if (newImages.length > 0) {
|
if (newImages.length > 0) {
|
||||||
let resultsContainer = document.querySelector('.images');
|
let resultsContainer = document.querySelector('.images');
|
||||||
newImages.forEach(imageDiv => {
|
newImages.forEach(imageDiv => {
|
||||||
// Append new images to the container
|
// Append new images to the container
|
||||||
resultsContainer.appendChild(imageDiv);
|
resultsContainer.appendChild(imageDiv);
|
||||||
|
|
||||||
// Get the img element
|
// Get the img element
|
||||||
let img = imageDiv.querySelector('img');
|
let img = imageDiv.querySelector('img');
|
||||||
if (img) {
|
if (img) {
|
||||||
|
@ -318,10 +320,12 @@
|
||||||
img.onerror = function() {
|
img.onerror = function() {
|
||||||
handleImageError(img);
|
handleImageError(img);
|
||||||
};
|
};
|
||||||
|
|
||||||
let id = img.getAttribute('data-id');
|
let id = img.getAttribute('data-id');
|
||||||
imageElements.push(img);
|
if (id) { // Only include if ID is not empty
|
||||||
imageIds.push(id);
|
imageElements.push(img);
|
||||||
|
imageIds.push(id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -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 => {
|
||||||
|
@ -409,20 +415,6 @@
|
||||||
// Remove 'js-enabled' class from content
|
// Remove 'js-enabled' class from content
|
||||||
document.getElementById('content').classList.remove('js-enabled');
|
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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -1,4 +1,3 @@
|
||||||
<!-- Images Grid -->
|
|
||||||
{{ range $index, $result := .Results }}
|
{{ range $index, $result := .Results }}
|
||||||
<div class="image">
|
<div class="image">
|
||||||
<img
|
<img
|
||||||
|
|
|
@ -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,38 +159,48 @@
|
||||||
</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 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>
|
||||||
|
|
||||||
<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-map" class="material-icons-round clickable" name="t" value="map">
|
||||||
<p>{{ translate "images" }}</p>
|
<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>
|
||||||
|
</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>
|
</form>
|
||||||
|
|
||||||
|
@ -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>
|
Loading…
Add table
Reference in a new issue