Initial favicon add

This commit is contained in:
partisan 2025-04-28 20:03:33 +02:00
parent 6445be87a9
commit bc89f5b819
8 changed files with 755 additions and 21 deletions

View file

@ -19,6 +19,7 @@ import (
"time"
"github.com/chai2010/webp"
"github.com/fyne-io/image/ico"
"golang.org/x/image/bmp"
"golang.org/x/image/tiff"
)
@ -139,6 +140,8 @@ func cacheImage(imageURL, imageID string, isThumbnail bool) (string, bool, error
// Decode the image based on the content type
var img image.Image
switch contentType {
case "image/x-icon", "image/vnd.microsoft.icon":
img, err = ico.Decode(bytes.NewReader(data))
case "image/jpeg":
img, err = jpeg.Decode(bytes.NewReader(data))
case "image/png":

View file

@ -8,6 +8,7 @@ import (
"html/template"
mathrand "math/rand"
"net/http"
"net/url"
"strings"
"time"
)
@ -36,6 +37,11 @@ type SearchEngine struct {
Func func(string, string, string, int) ([]SearchResult, time.Duration, error)
}
type LinkParts struct {
Domain template.HTML
Path template.HTML
}
// Helper function to render templates without elapsed time measurement
func renderTemplate(w http.ResponseWriter, tmplName string, data map[string]interface{}) {
// Generate icon paths for SVG and PNG, including a 1/10 chance for an alternate icon
@ -125,3 +131,48 @@ func FormatElapsedTime(elapsed time.Duration) string {
}
return fmt.Sprintf("%.2f %s", elapsed.Seconds(), Translate("seconds"))
}
func FormatURLParts(rawURL string) (domain, path string) {
parsed, err := url.Parse(rawURL)
if err != nil {
return rawURL, ""
}
domain = parsed.Host
if strings.HasPrefix(domain, "www.") {
domain = domain[4:]
}
// Clean up the path - remove empty segments and trailing slashes
path = strings.Trim(parsed.Path, "/")
pathSegments := strings.Split(path, "/")
// Filter out empty segments
var cleanSegments []string
for _, seg := range pathSegments {
if seg != "" {
cleanSegments = append(cleanSegments, seg)
}
}
path = strings.Join(cleanSegments, "/")
return domain, path
}
func FormatLinkHTML(rawURL string) LinkParts {
domain, path := FormatURLParts(rawURL)
if path == "" {
return LinkParts{
Domain: template.HTML(fmt.Sprintf(`<span class="result-domain">%s</span>`, template.HTMLEscapeString(domain))),
}
}
// Only add separators between non-empty path segments
pathDisplay := strings.ReplaceAll(path, "/", " ")
return LinkParts{
Domain: template.HTML(fmt.Sprintf(`<span class="result-domain">%s</span>`, template.HTMLEscapeString(domain))),
Path: template.HTML(fmt.Sprintf(`<span class="result-path"> %s</span>`, template.HTMLEscapeString(pathDisplay))),
}
}

549
favicon.go Normal file
View file

@ -0,0 +1,549 @@
package main
import (
"bytes"
"crypto/md5"
"crypto/tls"
"encoding/base64"
"encoding/hex"
"fmt"
"image"
"image/gif"
"image/jpeg"
"image/png"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"regexp"
"strings"
"sync"
"time"
"github.com/chai2010/webp"
"github.com/fyne-io/image/ico"
"golang.org/x/image/bmp"
"golang.org/x/image/draw"
"golang.org/x/image/tiff"
"golang.org/x/net/html"
)
var (
faviconCache = struct {
sync.RWMutex
m map[string]bool // tracks in-progress downloads
}{m: make(map[string]bool)}
// Common favicon paths to try
commonFaviconPaths = []string{
"/favicon.ico",
"/favicon.png",
"/favicon.jpg",
"/favicon.jpeg",
"/favicon.webp",
"/apple-touch-icon.png",
"/apple-touch-icon-precomposed.png",
}
// Regex to extract favicon URLs from HTML
iconLinkRegex = regexp.MustCompile(`<link[^>]+rel=["'](?:icon|shortcut icon|apple-touch-icon)["'][^>]+href=["']([^"']+)["']`)
)
// Generates a cache ID from URL
func faviconIDFromURL(rawURL string) string {
hasher := md5.New()
hasher.Write([]byte(rawURL))
return hex.EncodeToString(hasher.Sum(nil))
}
// Resolves favicon URL using multiple methods
func resolveFaviconURL(rawFavicon, pageURL string) (faviconURL, cacheID string) {
// Handle data URLs first
if strings.HasPrefix(rawFavicon, "data:image") {
parts := strings.SplitN(rawFavicon, ";base64,", 2)
if len(parts) == 2 {
data, err := base64.StdEncoding.DecodeString(parts[1])
if err == nil {
hasher := md5.New()
hasher.Write(data)
return rawFavicon, hex.EncodeToString(hasher.Sum(nil))
}
}
return "", "" // Invalid data URL
}
// Existing URL handling logic
if rawFavicon != "" && strings.HasPrefix(rawFavicon, "http") {
cacheID = faviconIDFromURL(rawFavicon)
return rawFavicon, cacheID
}
parsedPage, err := url.Parse(pageURL)
if err != nil {
return "", ""
}
// Method 1: Parse HTML
if favicon := findFaviconInHTML(pageURL); favicon != "" {
if strings.HasPrefix(favicon, "http") {
return favicon, faviconIDFromURL(favicon)
}
resolved := resolveRelativeURL(parsedPage, favicon)
return resolved, faviconIDFromURL(resolved)
}
// Method 2: Common paths
for _, path := range commonFaviconPaths {
testURL := "https://" + parsedPage.Host + path
if checkURLExists(testURL) {
return testURL, faviconIDFromURL(testURL)
}
}
// Method 3: HTTP headers
if headerIcon := findFaviconInHeaders(pageURL); headerIcon != "" {
if strings.HasPrefix(headerIcon, "http") {
return headerIcon, faviconIDFromURL(headerIcon)
}
resolved := resolveRelativeURL(parsedPage, headerIcon)
return resolved, faviconIDFromURL(resolved)
}
// Fallback
fallbackURL := "https://" + parsedPage.Host + "/favicon.ico"
return fallbackURL, faviconIDFromURL(fallbackURL)
}
// Checks HTTP headers for favicon links
func findFaviconInHeaders(pageURL string) string {
client := &http.Client{
Timeout: 3 * time.Second, // like 3 seconds for favicon should be enough
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
req, err := http.NewRequest("HEAD", pageURL, nil)
if err != nil {
return ""
}
// Add User-Agent
userAgent, err := GetUserAgent("findFaviconInHeaders")
if err != nil {
printWarn("Error getting User-Agent: %v", err)
}
req.Header.Set("User-Agent", userAgent)
resp, err := client.Do(req)
if err != nil {
return ""
}
defer resp.Body.Close()
// Check Link headers (common for favicons)
if links, ok := resp.Header["Link"]; ok {
for _, link := range links {
parts := strings.Split(link, ";")
if len(parts) < 2 {
continue
}
urlPart := strings.TrimSpace(parts[0])
if !strings.HasPrefix(urlPart, "<") || !strings.HasSuffix(urlPart, ">") {
continue
}
urlPart = urlPart[1 : len(urlPart)-1] // Remove < and >
for _, part := range parts[1:] {
part = strings.TrimSpace(part)
if strings.EqualFold(part, `rel="icon"`) ||
strings.EqualFold(part, `rel=icon`) ||
strings.EqualFold(part, `rel="shortcut icon"`) ||
strings.EqualFold(part, `rel=shortcut icon`) {
return urlPart
}
}
}
}
return ""
}
// Helper to resolve relative URLs
func resolveRelativeURL(base *url.URL, relative string) string {
if strings.HasPrefix(relative, "http") {
return relative
}
if strings.HasPrefix(relative, "//") {
return base.Scheme + ":" + relative
}
if strings.HasPrefix(relative, "/") {
return base.Scheme + "://" + base.Host + relative
}
return base.Scheme + "://" + base.Host + base.Path + "/" + relative
}
// Checks if a URL exists (returns 200 OK)
func checkURLExists(url string) bool {
client := &http.Client{
Timeout: 5 * time.Second,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
req, err := http.NewRequest("HEAD", url, nil)
if err != nil {
return false
}
// Add User-Agent
userAgent, err := GetUserAgent("Text-Search-Brave")
if err != nil {
printWarn("Error getting User-Agent: %v", err)
}
req.Header.Set("checkURLExists", userAgent)
resp, err := client.Do(req)
if err != nil {
return false
}
resp.Body.Close()
return resp.StatusCode == http.StatusOK
}
// Fetches HTML and looks for favicon links
func findFaviconInHTML(pageURL string) string {
client := &http.Client{
Timeout: 10 * time.Second,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
req, err := http.NewRequest("GET", pageURL, nil)
if err != nil {
return ""
}
// Add User-Agent
userAgent, err := GetUserAgent("findFaviconInHTML")
if err != nil {
printWarn("Error getting User-Agent: %v", err)
}
req.Header.Set("User-Agent", userAgent)
resp, err := client.Do(req)
if err != nil {
return ""
}
defer resp.Body.Close()
// Check if this is an AMP page
isAMP := false
for _, attr := range resp.Header["Link"] {
if strings.Contains(attr, "rel=\"amphtml\"") {
isAMP = true
break
}
}
// Parse HTML
doc, err := html.Parse(resp.Body)
if err != nil {
return ""
}
var faviconURL string
var findLinks func(*html.Node)
findLinks = func(n *html.Node) {
if n.Type == html.ElementNode && n.Data == "link" {
var rel, href string
for _, attr := range n.Attr {
switch attr.Key {
case "rel":
rel = attr.Val
case "href":
href = attr.Val
}
}
// Prioritize different favicon types
if href != "" {
switch rel {
case "icon", "shortcut icon", "apple-touch-icon", "apple-touch-icon-precomposed":
// For AMP pages, prefer the non-versioned URL if possible
if isAMP {
if u, err := url.Parse(href); err == nil {
u.RawQuery = "" // Remove query parameters
href = u.String()
}
}
if faviconURL == "" || // First found
rel == "apple-touch-icon" || // Prefer apple-touch-icon
rel == "icon" { // Then regular icon
faviconURL = href
}
}
}
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
findLinks(c)
}
}
findLinks(doc)
return faviconURL
}
// Get proxy URL (cached) - remains mostly the same
func getFaviconProxyURL(rawFavicon, pageURL string) string {
// First try cache without any locks
cacheID := faviconIDFromURL(pageURL) // Simple hash of pageURL
filename := fmt.Sprintf("%s_thumb.webp", cacheID)
cachedPath := filepath.Join(config.DriveCache.Path, "images", filename)
if _, err := os.Stat(cachedPath); err == nil {
return fmt.Sprintf("/image/%s_thumb.webp", cacheID)
}
// Cache miss - resolve favicon URL (may hit network)
faviconURL, cacheID := resolveFaviconURL(rawFavicon, pageURL)
if faviconURL == "" || cacheID == "" {
return "/static/images/missing.svg"
}
// Recheck cache after resolution (in case another request cached it)
if _, err := os.Stat(cachedPath); err == nil {
return fmt.Sprintf("/image/%s_thumb.webp", cacheID)
}
// Check download status with lock
faviconCache.RLock()
downloading := faviconCache.m[cacheID]
faviconCache.RUnlock()
if !downloading {
faviconCache.Lock()
faviconCache.m[cacheID] = true
faviconCache.Unlock()
go func() {
defer func() {
faviconCache.Lock()
delete(faviconCache.m, cacheID)
faviconCache.Unlock()
}()
cacheFavicon(faviconURL, cacheID)
}()
}
return fmt.Sprintf("/image/%s_thumb.webp", cacheID)
}
// Caches favicon, always saving *_thumb.webp
func cacheFavicon(imageURL, imageID string) (string, bool, error) {
if imageURL == "" {
recordInvalidImageID(imageID)
return "", false, fmt.Errorf("empty image URL for image ID %s", imageID)
}
filename := fmt.Sprintf("%s_thumb.webp", imageID)
imageCacheDir := filepath.Join(config.DriveCache.Path, "images")
if err := os.MkdirAll(imageCacheDir, 0755); err != nil {
return "", false, fmt.Errorf("couldn't create images folder: %v", err)
}
cachedImagePath := filepath.Join(imageCacheDir, filename)
tempImagePath := cachedImagePath + ".tmp"
// Already cached?
if _, err := os.Stat(cachedImagePath); err == nil {
return cachedImagePath, true, nil
}
cachingImagesMu.Lock()
if _, exists := cachingImages[imageURL]; !exists {
cachingImages[imageURL] = &sync.Mutex{}
}
mu := cachingImages[imageURL]
cachingImagesMu.Unlock()
mu.Lock()
defer mu.Unlock()
// Recheck after lock
if _, err := os.Stat(cachedImagePath); err == nil {
return cachedImagePath, true, nil
}
cachingSemaphore <- struct{}{}
defer func() { <-cachingSemaphore }()
var data []byte
var contentType string
// Handle data URLs
if strings.HasPrefix(imageURL, "data:") {
commaIndex := strings.Index(imageURL, ",")
if commaIndex == -1 {
recordInvalidImageID(imageID)
return "", false, fmt.Errorf("invalid data URL: no comma")
}
headerPart := imageURL[:commaIndex]
dataPart := imageURL[commaIndex+1:]
mediaType := "text/plain"
base64Encoded := false
if strings.HasPrefix(headerPart, "data:") {
mediaTypePart := headerPart[5:]
mediaTypeParts := strings.SplitN(mediaTypePart, ";", 2)
mediaType = mediaTypeParts[0]
if len(mediaTypeParts) > 1 {
for _, param := range strings.Split(mediaTypeParts[1], ";") {
param = strings.TrimSpace(param)
if param == "base64" {
base64Encoded = true
}
}
}
}
if base64Encoded {
data, _ = base64.StdEncoding.DecodeString(dataPart)
} else {
decodedStr, err := url.QueryUnescape(dataPart)
if err != nil {
data = []byte(dataPart)
} else {
data = []byte(decodedStr)
}
}
contentType = mediaType
} else {
// Download from HTTP URL
client := &http.Client{
Timeout: 15 * time.Second,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
req, err := http.NewRequest("GET", imageURL, nil)
if err != nil {
recordInvalidImageID(imageID)
return "", false, err
}
// Add User-Agent
userAgent, err := GetUserAgent("Text-Search-Brave")
if err != nil {
printWarn("Error getting User-Agent: %v", err)
}
req.Header.Set("User-Agent", userAgent)
resp, err := client.Do(req)
if err != nil {
recordInvalidImageID(imageID)
return "", false, err
}
defer resp.Body.Close()
data, err = io.ReadAll(resp.Body)
if err != nil {
recordInvalidImageID(imageID)
return "", false, err
}
contentType = http.DetectContentType(data)
}
if !strings.HasPrefix(contentType, "image/") {
recordInvalidImageID(imageID)
return "", false, fmt.Errorf("URL did not return an image: %s", imageURL)
}
// SVG special case
if contentType == "image/svg+xml" {
err := os.WriteFile(tempImagePath, data, 0644)
if err != nil {
recordInvalidImageID(imageID)
return "", false, err
}
err = os.Rename(tempImagePath, cachedImagePath)
if err != nil {
recordInvalidImageID(imageID)
return "", false, err
}
cachingImagesMu.Lock()
delete(cachingImages, imageURL)
cachingImagesMu.Unlock()
return cachedImagePath, true, nil
}
// Decode image
var img image.Image
var err error
switch contentType {
case "image/x-icon", "image/vnd.microsoft.icon":
img, err = ico.Decode(bytes.NewReader(data))
case "image/jpeg":
img, err = jpeg.Decode(bytes.NewReader(data))
case "image/png":
img, err = png.Decode(bytes.NewReader(data))
case "image/gif":
img, err = gif.Decode(bytes.NewReader(data))
case "image/webp":
img, err = webp.Decode(bytes.NewReader(data))
case "image/bmp":
img, err = bmp.Decode(bytes.NewReader(data))
case "image/tiff":
img, err = tiff.Decode(bytes.NewReader(data))
default:
recordInvalidImageID(imageID)
return "", false, fmt.Errorf("unsupported image type: %s", contentType)
}
if err != nil {
recordInvalidImageID(imageID)
return "", false, err
}
// Resize
maxSize := 16
width := img.Bounds().Dx()
height := img.Bounds().Dy()
if width > maxSize || height > maxSize {
dst := image.NewRGBA(image.Rect(0, 0, maxSize, maxSize))
draw.ApproxBiLinear.Scale(dst, dst.Bounds(), img, img.Bounds(), draw.Over, nil)
img = dst
}
// Save as WebP
outFile, err := os.Create(tempImagePath)
if err != nil {
recordInvalidImageID(imageID)
return "", false, err
}
defer outFile.Close()
options := &webp.Options{Lossless: false, Quality: 80}
err = webp.Encode(outFile, img, options)
if err != nil {
recordInvalidImageID(imageID)
return "", false, err
}
err = os.Rename(tempImagePath, cachedImagePath)
if err != nil {
recordInvalidImageID(imageID)
return "", false, err
}
cachingImagesMu.Lock()
delete(cachingImages, imageURL)
cachingImagesMu.Unlock()
return cachedImagePath, true, nil
}

3
go.mod
View file

@ -17,6 +17,7 @@ require (
github.com/blevesearch/bleve/v2 v2.4.4
github.com/chromedp/cdproto v0.0.0-20241022234722-4d5d5faf59fb
github.com/chromedp/chromedp v0.11.2
github.com/fyne-io/image v0.1.1
github.com/go-shiori/go-readability v0.0.0-20241012063810-92284fa8a71f
golang.org/x/net v0.33.0
)
@ -55,11 +56,11 @@ require (
github.com/golang/snappy v0.0.4 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mschoch/smat v0.2.0 // indirect
github.com/stretchr/testify v1.9.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.etcd.io/bbolt v1.3.11 // indirect
golang.org/x/sys v0.28.0 // indirect

8
go.sum
View file

@ -56,6 +56,8 @@ github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHG
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fyne-io/image v0.1.1 h1:WH0z4H7qfvNUw5l4p3bC1q70sa5+YWVt6HCj7y4VNyA=
github.com/fyne-io/image v0.1.1/go.mod h1:xrfYBh6yspc+KjkgdZU/ifUC9sPA5Iv7WYUBzQKK7JM=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
@ -84,6 +86,8 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M=
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
github.com/leonelquinteros/gotext v1.7.0 h1:jcJmF4AXqyamP7vuw2MMIKs+O3jAEmvrc5JQiI8Ht/8=
@ -111,8 +115,8 @@ github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMT
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=

View file

@ -1309,6 +1309,96 @@ p {
text-shadow: 1px 1px 2px var(--border) !important; /* Adjust text shadow */
}
/* Favicon styling */
.result_header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.favicon-container {
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
border: 1px solid var(--fg);
border-radius: 50%;
padding: 2px;
flex-shrink: 0;
}
.favicon {
width: 16px;
height: 16px;
object-fit: contain;
margin-bottom: 2px;
border-radius: 3px;
margin-left: -0.5px; /* this is just lovely, I cannot express ho happy this makes me */
margin-top: 1.5px;
}
/* Result link styling */
.result-link {
color: var(--fg);
font-size: 14px;
text-decoration: none;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.result-link:hover {
text-decoration: underline;
}
/* Result item spacing */
.result_item {
margin-bottom: 1.5rem;
}
.result-title h3 {
margin: 4px 0;
font-weight: 400;
}
.result-description {
margin: 4px 0 0 0;
color: var(--font-fg);
line-height: 1.4;
}
.results br {
display: none;
}
.result-url {
font-size: 14px;
color: var(--fg);
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 4px;
}
.result-domain {
color: var(--fg);
font-weight: 600;
}
.result-path {
color: var(--font-fg);
font-weight: 500;
opacity: 0.8;
}
/*
.result-path::before {
content: "";
margin: 0 4px;
opacity: 0.6;
} */
body, h1, p, a, input, button {
color: var(--text-color); /* Applies the text color based on theme */
background-color: var(--background-color); /* Applies the background color based on theme */

View file

@ -141,24 +141,35 @@
<p class="fetched fetched_dif_files fetched_tor">{{ translate "fetched_in" .Fetched }}</p>
<div class="results" id="results">
{{if .Results}}
{{range .Results}}
<div class="result_item">
<a id="link" href="{{.URL}}">{{.URL}}</a>
<a href="{{.URL}}"><h3>{{.Header}}</h3></a>
<p>{{.Description}}</p>
{{ if .Results }}
{{ range .Results }}
<div class="result_item">
<div class="result_header">
<div class="favicon-container">
<img src="{{ .FaviconURL }}" alt="🌐" class="favicon">
</div>
<div class="result-url">
{{ .PrettyLink.Domain }}
{{ if .PrettyLink.Path }}
{{ .PrettyLink.Path }}
{{ end }}
</div>
</div>
<br>
{{end}}
{{else if .NoResults}}
<a href="{{ .URL }}" class="result-title"><h3>{{ .Header }}</h3></a>
<p class="result-description">{{ .Description }}</p>
</div>
{{ end }}
{{ else if .NoResults }}
<div class="no-results-found">
{{ translate "no_results_found" .Query }}<br>
{{ translate "suggest_rephrase" }}
</div>
{{else}}
<div class="no-results-found">{{ translate "no_more_results" }}</div>
{{end}}
</div>
{{ else }}
<div class="no-results-found">
{{ translate "no_more_results" }}
</div>
{{ end }}
</div>
<div id="message-bottom-right" class="message-bottom-right">
<span id="loading-text">{{ translate "searching_for_new_results" }}</span><span class="dot">.</span><span class="dot">.</span><span class="dot">.</span>
</div>

35
text.go
View file

@ -33,7 +33,13 @@ func initTextEngines() {
func HandleTextSearch(w http.ResponseWriter, settings UserSettings, query string, page int) {
startTime := time.Now()
cacheKey := CacheKey{Query: query, Page: page, Safe: settings.SafeSearch == "active", Lang: settings.SearchLanguage, Type: "text"}
cacheKey := CacheKey{
Query: query,
Page: page,
Safe: settings.SafeSearch == "active",
Lang: settings.SearchLanguage,
Type: "text",
}
combinedResults := getTextResultsFromCacheOrFetch(cacheKey, query, settings.SafeSearch, settings.SearchLanguage, page)
hasPrevPage := page > 1
@ -46,13 +52,32 @@ func HandleTextSearch(w http.ResponseWriter, settings UserSettings, query string
elapsedTime := time.Since(startTime)
// Prepare the data to pass to the template
// Prepare safe decorated results
type DecoratedResult struct {
TextSearchResult
FaviconURL string
PrettyLink LinkParts
}
var decoratedResults []DecoratedResult
for _, r := range combinedResults {
if r.URL == "" {
continue
}
decoratedResults = append(decoratedResults, DecoratedResult{
TextSearchResult: r,
FaviconURL: getFaviconProxyURL("", r.URL),
PrettyLink: FormatLinkHTML(r.URL),
})
}
data := map[string]interface{}{
"Results": combinedResults,
"Results": decoratedResults,
"Query": query,
"Fetched": FormatElapsedTime(elapsedTime),
"Page": page,
"HasPrevPage": page > 1,
"HasPrevPage": hasPrevPage,
"HasNextPage": len(combinedResults) >= 50,
"NoResults": len(combinedResults) == 0,
"LanguageOptions": languageOptions,
@ -63,7 +88,7 @@ func HandleTextSearch(w http.ResponseWriter, settings UserSettings, query string
"Trans": Translate,
}
// Render the template without measuring time
// Render the template
renderTemplate(w, "text.html", data)
}