Compare commits
12 commits
a4285b8939
...
b159980a26
Author | SHA1 | Date | |
---|---|---|---|
b159980a26 | |||
|
d414cf5ce4 | ||
|
4d6b99c67e | ||
|
18fca7dce2 | ||
|
a02ccaa2f0 | ||
|
56ce016db9 | ||
|
9aabff0a12 | ||
|
da6fae4b4b | ||
|
f7df15e47f | ||
|
6ef817a2b0 | ||
|
14b4f3aaf5 | ||
|
980de2b59b |
23 changed files with 1009 additions and 216 deletions
2
files.go
2
files.go
|
@ -71,6 +71,7 @@ func handleFileSearch(w http.ResponseWriter, settings UserSettings, query string
|
||||||
CurrentLang string
|
CurrentLang string
|
||||||
Theme string
|
Theme string
|
||||||
Safe string
|
Safe string
|
||||||
|
IsThemeDark bool
|
||||||
}{
|
}{
|
||||||
Results: combinedResults,
|
Results: combinedResults,
|
||||||
Query: query,
|
Query: query,
|
||||||
|
@ -84,6 +85,7 @@ func handleFileSearch(w http.ResponseWriter, settings UserSettings, query string
|
||||||
CurrentLang: settings.Language,
|
CurrentLang: settings.Language,
|
||||||
Theme: settings.Theme,
|
Theme: settings.Theme,
|
||||||
Safe: settings.SafeSearch,
|
Safe: settings.SafeSearch,
|
||||||
|
IsThemeDark: settings.IsThemeDark,
|
||||||
}
|
}
|
||||||
|
|
||||||
// // Debugging: Print results before rendering template
|
// // Debugging: Print results before rendering template
|
||||||
|
|
|
@ -115,6 +115,7 @@ func handleForumsSearch(w http.ResponseWriter, settings UserSettings, query stri
|
||||||
CurrentLang string
|
CurrentLang string
|
||||||
Theme string
|
Theme string
|
||||||
Safe string
|
Safe string
|
||||||
|
IsThemeDark bool
|
||||||
}{
|
}{
|
||||||
Query: query,
|
Query: query,
|
||||||
Results: results,
|
Results: results,
|
||||||
|
@ -125,6 +126,7 @@ func handleForumsSearch(w http.ResponseWriter, settings UserSettings, query stri
|
||||||
CurrentLang: settings.Language,
|
CurrentLang: settings.Language,
|
||||||
Theme: settings.Theme,
|
Theme: settings.Theme,
|
||||||
Safe: settings.SafeSearch,
|
Safe: settings.SafeSearch,
|
||||||
|
IsThemeDark: settings.IsThemeDark,
|
||||||
}
|
}
|
||||||
|
|
||||||
funcMap := template.FuncMap{
|
funcMap := template.FuncMap{
|
||||||
|
|
237
images-deviantart.go
Normal file
237
images-deviantart.go
Normal file
|
@ -0,0 +1,237 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/PuerkitoBio/goquery"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NextPageCache is a specialized cache for storing next page links
|
||||||
|
type NextPageCache struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
links map[string]string
|
||||||
|
expiration time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNextPageCache creates a new NextPageCache with a specified expiration duration
|
||||||
|
func NewNextPageCache(expiration time.Duration) *NextPageCache {
|
||||||
|
return &NextPageCache{
|
||||||
|
links: make(map[string]string),
|
||||||
|
expiration: expiration,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get retrieves the next page link for a given key from the cache
|
||||||
|
func (npc *NextPageCache) Get(key CacheKey) (string, bool) {
|
||||||
|
npc.mu.Lock()
|
||||||
|
defer npc.mu.Unlock()
|
||||||
|
|
||||||
|
link, exists := npc.links[npc.keyToString(key)]
|
||||||
|
if !exists {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
return link, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set stores the next page link for a given key in the cache
|
||||||
|
// Idk it maybye worth it to use "cache.go" for this
|
||||||
|
func (npc *NextPageCache) Set(key CacheKey, link string) {
|
||||||
|
npc.mu.Lock()
|
||||||
|
defer npc.mu.Unlock()
|
||||||
|
|
||||||
|
npc.links[npc.keyToString(key)] = link
|
||||||
|
}
|
||||||
|
|
||||||
|
// keyToString converts a CacheKey to a string representation
|
||||||
|
func (npc *NextPageCache) keyToString(key CacheKey) string {
|
||||||
|
return fmt.Sprintf("%s|%d|%t|%s|%s", key.Query, key.Page, key.Safe, key.Lang, key.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
nextPageCache = NewNextPageCache(6 * time.Hour) // Cache with 6-hour expiration
|
||||||
|
)
|
||||||
|
|
||||||
|
// PerformDeviantArtImageSearch performs a search on DeviantArt and returns a list of image results
|
||||||
|
func PerformDeviantArtImageSearch(query, safe, lang string, page int) ([]ImageSearchResult, time.Duration, error) {
|
||||||
|
startTime := time.Now()
|
||||||
|
|
||||||
|
cacheKey := CacheKey{
|
||||||
|
Query: query,
|
||||||
|
Page: page,
|
||||||
|
Safe: safe == "active",
|
||||||
|
Lang: lang,
|
||||||
|
Type: "deviantart",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the next page link is cached
|
||||||
|
var searchURL string
|
||||||
|
if page > 1 {
|
||||||
|
if nextPageLink, found := nextPageCache.Get(cacheKey); found {
|
||||||
|
searchURL = nextPageLink
|
||||||
|
} else {
|
||||||
|
return nil, 0, fmt.Errorf("next page link not found in cache")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
searchURL = buildDeviantArtSearchURL(query, page)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the User-Agent string
|
||||||
|
DeviantArtImageUserAgent, err := GetUserAgent("Image-Search-DeviantArt")
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make the HTTP request with User-Agent header
|
||||||
|
client := &http.Client{}
|
||||||
|
req, err := http.NewRequest("GET", searchURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, fmt.Errorf("creating request: %v", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("User-Agent", DeviantArtImageUserAgent)
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, fmt.Errorf("making request: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, 0, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the HTML document
|
||||||
|
doc, err := goquery.NewDocumentFromReader(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, fmt.Errorf("loading HTML document: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Channel to receive valid image results
|
||||||
|
resultsChan := make(chan ImageSearchResult)
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
// Extract data using goquery
|
||||||
|
doc.Find("div._2pZkk div div a").Each(func(i int, s *goquery.Selection) {
|
||||||
|
// Skip images that are blurred (premium content)
|
||||||
|
premiumText := s.Find("../div/div/div").Text()
|
||||||
|
if strings.Contains(premiumText, "Watch the artist to view this deviation") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract image source, fallback on data-src if necessary
|
||||||
|
imgSrc, exists := s.Find("div img").Attr("srcset")
|
||||||
|
if !exists {
|
||||||
|
imgSrc, exists = s.Find("div img").Attr("data-src")
|
||||||
|
}
|
||||||
|
if !exists || imgSrc == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
imgSrc = strings.Split(imgSrc, " ")[0]
|
||||||
|
parsedURL, err := url.Parse(imgSrc)
|
||||||
|
if err == nil {
|
||||||
|
parts := strings.Split(parsedURL.Path, "/v1")
|
||||||
|
parsedURL.Path = parts[0]
|
||||||
|
imgSrc = parsedURL.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract URL and title
|
||||||
|
resultURL := s.AttrOr("href", "")
|
||||||
|
title := s.AttrOr("aria-label", "")
|
||||||
|
|
||||||
|
// Only proceed if title, URL, and img_src are not empty
|
||||||
|
if title != "" && resultURL != "" && imgSrc != "" {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(imgSrc, resultURL, title string) {
|
||||||
|
defer wg.Done()
|
||||||
|
// Verify if the image URL is accessible
|
||||||
|
if isValidImageURL(imgSrc, DeviantArtImageUserAgent, resultURL) {
|
||||||
|
resultsChan <- ImageSearchResult{
|
||||||
|
Title: strings.TrimSpace(title),
|
||||||
|
Media: imgSrc,
|
||||||
|
Width: 0,
|
||||||
|
Height: 0,
|
||||||
|
Source: resultURL,
|
||||||
|
ThumbProxy: imgSrc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}(imgSrc, resultURL, title)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Close the results channel when all goroutines are done
|
||||||
|
go func() {
|
||||||
|
wg.Wait()
|
||||||
|
close(resultsChan)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Collect results from the channel
|
||||||
|
var results []ImageSearchResult
|
||||||
|
for result := range resultsChan {
|
||||||
|
results = append(results, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache the next page link, if any
|
||||||
|
nextPageLink := doc.Find("a._1OGeq").Last().AttrOr("href", "")
|
||||||
|
if nextPageLink != "" {
|
||||||
|
nextPageCache.Set(cacheKey, nextPageLink)
|
||||||
|
}
|
||||||
|
|
||||||
|
duration := time.Since(startTime)
|
||||||
|
|
||||||
|
// Check if the number of results is one or less
|
||||||
|
if len(results) == 0 {
|
||||||
|
return nil, duration, fmt.Errorf("no images found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, duration, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildDeviantArtSearchURL builds the search URL for DeviantArt
|
||||||
|
func buildDeviantArtSearchURL(query string, page int) string {
|
||||||
|
baseURL := "https://www.deviantart.com/search"
|
||||||
|
params := url.Values{}
|
||||||
|
params.Add("q", query)
|
||||||
|
return baseURL + "?" + params.Encode()
|
||||||
|
}
|
||||||
|
|
||||||
|
// isValidImageURL checks if the image URL is accessible with the provided User-Agent
|
||||||
|
func isValidImageURL(imgSrc, userAgent, referer string) bool {
|
||||||
|
client := &http.Client{}
|
||||||
|
req, err := http.NewRequest("HEAD", imgSrc, nil)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set headers to mimic a regular browser request
|
||||||
|
req.Header.Set("User-Agent", userAgent)
|
||||||
|
req.Header.Set("Referer", referer)
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
return resp.StatusCode == http.StatusOK
|
||||||
|
}
|
||||||
|
|
||||||
|
// // Example usage:
|
||||||
|
// func main() {
|
||||||
|
// results, duration, err := PerformDeviantArtImageSearch("kittens", "false", "en", 1)
|
||||||
|
// if err != nil {
|
||||||
|
// fmt.Println("Error:", err)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fmt.Printf("Search took: %v\n", duration)
|
||||||
|
// fmt.Printf("Total results: %d\n", len(results))
|
||||||
|
// for _, result := range results {
|
||||||
|
// fmt.Printf("Title: %s\nThumbnail: %s\nMedia: %s\nSource (Original Image URL): %s\n\n",
|
||||||
|
// result.Title, result.Thumbnail, result.Media, result.Source)
|
||||||
|
// }
|
||||||
|
// }
|
|
@ -24,6 +24,64 @@ type QwantAPIResponse struct {
|
||||||
} `json:"data"`
|
} `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ConvertToQwantLocale(langCode string) string {
|
||||||
|
langMap := map[string]string{
|
||||||
|
"en": "en_us", // English
|
||||||
|
"af": "en_us", // Afrikaans (no direct match, default to English)
|
||||||
|
"ar": "ar", // Arabic
|
||||||
|
"hy": "en_us", // Armenian (no direct match, default to English)
|
||||||
|
"be": "en_us", // Belarusian (no direct match, default to English)
|
||||||
|
"bg": "bg_bg", // Bulgarian
|
||||||
|
"ca": "ca_es", // Catalan
|
||||||
|
"zh-CN": "zh_cn", // Chinese Simplified
|
||||||
|
"zh-TW": "zh_hk", // Chinese Traditional
|
||||||
|
"hr": "en_us", // Croatian (no direct match, default to English)
|
||||||
|
"cs": "cs_cz", // Czech
|
||||||
|
"da": "da_dk", // Danish
|
||||||
|
"nl": "nl_nl", // Dutch
|
||||||
|
"eo": "en_us", // Esperanto (no direct match, default to English)
|
||||||
|
"et": "et_ee", // Estonian
|
||||||
|
"tl": "en_us", // Tagalog (no direct match, default to English)
|
||||||
|
"fi": "fi_fi", // Finnish
|
||||||
|
"fr": "fr_fr", // French
|
||||||
|
"de": "de_de", // German
|
||||||
|
"el": "el_gr", // Greek
|
||||||
|
"iw": "he_il", // Hebrew
|
||||||
|
"hi": "en_us", // Hindi (no direct match, default to English)
|
||||||
|
"hu": "hu_hu", // Hungarian
|
||||||
|
"is": "en_us", // Icelandic (no direct match, default to English)
|
||||||
|
"id": "en_us", // Indonesian (no direct match, default to English)
|
||||||
|
"it": "it_it", // Italian
|
||||||
|
"ja": "ja_jp", // Japanese
|
||||||
|
"ko": "ko_kr", // Korean
|
||||||
|
"lv": "en_us", // Latvian (no direct match, default to English)
|
||||||
|
"lt": "en_us", // Lithuanian (no direct match, default to English)
|
||||||
|
"no": "nb_no", // Norwegian
|
||||||
|
"fa": "en_us", // Persian (no direct match, default to English)
|
||||||
|
"pl": "pl_pl", // Polish
|
||||||
|
"pt": "pt_pt", // Portuguese
|
||||||
|
"ro": "ro_ro", // Romanian
|
||||||
|
"ru": "ru_ru", // Russian
|
||||||
|
"sr": "en_us", // Serbian (no direct match, default to English)
|
||||||
|
"sk": "en_us", // Slovak (no direct match, default to English)
|
||||||
|
"sl": "en_us", // Slovenian (no direct match, default to English)
|
||||||
|
"es": "es_es", // Spanish
|
||||||
|
"sw": "en_us", // Swahili (no direct match, default to English)
|
||||||
|
"sv": "sv_se", // Swedish
|
||||||
|
"th": "th_th", // Thai
|
||||||
|
"tr": "tr_tr", // Turkish
|
||||||
|
"uk": "en_us", // Ukrainian (no direct match, default to English)
|
||||||
|
"vi": "en_us", // Vietnamese (no direct match, default to English)
|
||||||
|
"": "en_us", // Default to English if no language is provided
|
||||||
|
}
|
||||||
|
|
||||||
|
if qwantLocale, exists := langMap[langCode]; exists {
|
||||||
|
return qwantLocale
|
||||||
|
}
|
||||||
|
printWarn("Qwant locale code missing: %v, defaulting to: en_us", langCode)
|
||||||
|
return "en_us" // Default fallback
|
||||||
|
}
|
||||||
|
|
||||||
// PerformQwantImageSearch performs an image search on Qwant and returns the results.
|
// PerformQwantImageSearch performs an image search on Qwant and returns the results.
|
||||||
func PerformQwantImageSearch(query, safe, lang string, page int) ([]ImageSearchResult, time.Duration, error) {
|
func PerformQwantImageSearch(query, safe, lang string, page int) ([]ImageSearchResult, time.Duration, error) {
|
||||||
startTime := time.Now() // Start the timer
|
startTime := time.Now() // Start the timer
|
||||||
|
@ -36,13 +94,16 @@ func PerformQwantImageSearch(query, safe, lang string, page int) ([]ImageSearchR
|
||||||
offset = (page - 1) * resultsPerPage
|
offset = (page - 1) * resultsPerPage
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure count + offset is within acceptable limits
|
||||||
|
if offset+resultsPerPage > 250 {
|
||||||
|
return nil, 0, fmt.Errorf("count + offset must be lower than 250 for quant")
|
||||||
|
}
|
||||||
|
|
||||||
if safe == "" {
|
if safe == "" {
|
||||||
safe = "0"
|
safe = "0"
|
||||||
}
|
}
|
||||||
|
|
||||||
if lang == "" {
|
lang = ConvertToQwantLocale(lang)
|
||||||
lang = "en_CA"
|
|
||||||
}
|
|
||||||
|
|
||||||
apiURL := fmt.Sprintf("https://api.qwant.com/v3/search/images?t=images&q=%s&count=%d&locale=%s&offset=%d&device=desktop&tgp=2&safesearch=%s",
|
apiURL := fmt.Sprintf("https://api.qwant.com/v3/search/images?t=images&q=%s&count=%d&locale=%s&offset=%d&device=desktop&tgp=2&safesearch=%s",
|
||||||
url.QueryEscape(query),
|
url.QueryEscape(query),
|
||||||
|
@ -58,7 +119,7 @@ func PerformQwantImageSearch(query, safe, lang string, page int) ([]ImageSearchR
|
||||||
return nil, 0, fmt.Errorf("creating request: %v", err)
|
return nil, 0, fmt.Errorf("creating request: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ImageUserAgent, err := GetUserAgent("Image-Search")
|
ImageUserAgent, err := GetUserAgent("Image-Search-Quant")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
@ -87,7 +148,7 @@ func PerformQwantImageSearch(query, safe, lang string, page int) ([]ImageSearchR
|
||||||
Title: item.Title,
|
Title: item.Title,
|
||||||
Media: item.Media,
|
Media: item.Media,
|
||||||
Source: item.Url,
|
Source: item.Url,
|
||||||
ThumbProxy: "/img_proxy?url=" + url.QueryEscape(item.Media),
|
ThumbProxy: "/img_proxy?url=" + url.QueryEscape(item.Media), // New proxy not exactly working as intended
|
||||||
Width: item.Width,
|
Width: item.Width,
|
||||||
Height: item.Height,
|
Height: item.Height,
|
||||||
})
|
})
|
||||||
|
|
|
@ -13,6 +13,7 @@ var imageSearchEngines []SearchEngine
|
||||||
func init() {
|
func init() {
|
||||||
imageSearchEngines = []SearchEngine{
|
imageSearchEngines = []SearchEngine{
|
||||||
{Name: "Qwant", Func: wrapImageSearchFunc(PerformQwantImageSearch), Weight: 1},
|
{Name: "Qwant", Func: wrapImageSearchFunc(PerformQwantImageSearch), Weight: 1},
|
||||||
|
{Name: "DeviantArt", Func: wrapImageSearchFunc(PerformDeviantArtImageSearch), Weight: 2},
|
||||||
{Name: "Bing", Func: wrapImageSearchFunc(PerformBingImageSearch), Weight: 2}, // Bing sometimes returns with low amount of images, this leads to danamica page loading not working
|
{Name: "Bing", Func: wrapImageSearchFunc(PerformBingImageSearch), Weight: 2}, // Bing sometimes returns with low amount of images, this leads to danamica page loading not working
|
||||||
{Name: "Imgur", Func: wrapImageSearchFunc(PerformImgurImageSearch), Weight: 3},
|
{Name: "Imgur", Func: wrapImageSearchFunc(PerformImgurImageSearch), Weight: 3},
|
||||||
}
|
}
|
||||||
|
@ -44,6 +45,7 @@ func handleImageSearch(w http.ResponseWriter, settings UserSettings, query strin
|
||||||
CurrentLang string
|
CurrentLang string
|
||||||
Theme string
|
Theme string
|
||||||
Safe string
|
Safe string
|
||||||
|
IsThemeDark bool
|
||||||
}{
|
}{
|
||||||
Results: combinedResults,
|
Results: combinedResults,
|
||||||
Query: query,
|
Query: query,
|
||||||
|
@ -56,6 +58,7 @@ func handleImageSearch(w http.ResponseWriter, settings UserSettings, query strin
|
||||||
CurrentLang: settings.Language,
|
CurrentLang: settings.Language,
|
||||||
Theme: settings.Theme,
|
Theme: settings.Theme,
|
||||||
Safe: settings.SafeSearch,
|
Safe: settings.SafeSearch,
|
||||||
|
IsThemeDark: settings.IsThemeDark,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = tmpl.Execute(w, data)
|
err = tmpl.Execute(w, data)
|
||||||
|
|
121
main.go
121
main.go
|
@ -17,78 +17,92 @@ type LanguageOption struct {
|
||||||
var settings UserSettings
|
var settings UserSettings
|
||||||
|
|
||||||
var languageOptions = []LanguageOption{
|
var languageOptions = []LanguageOption{
|
||||||
{Code: "", Name: "Any Language"},
|
{Code: "", Name: "Auto-detect"},
|
||||||
{Code: "lang_en", Name: "English"},
|
{Code: "en", Name: "English"},
|
||||||
{Code: "lang_af", Name: "Afrikaans"},
|
{Code: "af", Name: "Afrikaans"},
|
||||||
{Code: "lang_ar", Name: "العربية (Arabic)"},
|
{Code: "ar", Name: "العربية (Arabic)"},
|
||||||
{Code: "lang_hy", Name: "Հայերեն (Armenian)"},
|
{Code: "hy", Name: "Հայերեն (Armenian)"},
|
||||||
{Code: "lang_be", Name: "Беларуская (Belarusian)"},
|
{Code: "be", Name: "Беларуская (Belarusian)"},
|
||||||
{Code: "lang_bg", Name: "български (Bulgarian)"},
|
{Code: "bg", Name: "български (Bulgarian)"},
|
||||||
{Code: "lang_ca", Name: "Català (Catalan)"},
|
{Code: "ca", Name: "Català (Catalan)"},
|
||||||
{Code: "lang_zh-CN", Name: "中文 (简体) (Chinese Simplified)"},
|
{Code: "zh-CN", Name: "中文 (简体) (Chinese Simplified)"},
|
||||||
{Code: "lang_zh-TW", Name: "中文 (繁體) (Chinese Traditional)"},
|
{Code: "zh-TW", Name: "中文 (繁體) (Chinese Traditional)"},
|
||||||
{Code: "lang_hr", Name: "Hrvatski (Croatian)"},
|
{Code: "hr", Name: "Hrvatski (Croatian)"},
|
||||||
{Code: "lang_cs", Name: "Čeština (Czech)"},
|
{Code: "cs", Name: "Čeština (Czech)"},
|
||||||
{Code: "lang_da", Name: "Dansk (Danish)"},
|
{Code: "da", Name: "Dansk (Danish)"},
|
||||||
{Code: "lang_nl", Name: "Nederlands (Dutch)"},
|
{Code: "nl", Name: "Nederlands (Dutch)"},
|
||||||
{Code: "lang_eo", Name: "Esperanto"},
|
{Code: "eo", Name: "Esperanto"},
|
||||||
{Code: "lang_et", Name: "Eesti (Estonian)"},
|
{Code: "et", Name: "Eesti (Estonian)"},
|
||||||
{Code: "lang_tl", Name: "Filipino (Tagalog)"},
|
{Code: "tl", Name: "Filipino (Tagalog)"},
|
||||||
{Code: "lang_fi", Name: "Suomi (Finnish)"},
|
{Code: "fi", Name: "Suomi (Finnish)"},
|
||||||
{Code: "lang_fr", Name: "Français (French)"},
|
{Code: "fr", Name: "Français (French)"},
|
||||||
{Code: "lang_de", Name: "Deutsch (German)"},
|
{Code: "de", Name: "Deutsch (German)"},
|
||||||
{Code: "lang_el", Name: "Ελληνικά (Greek)"},
|
{Code: "el", Name: "Ελληνικά (Greek)"},
|
||||||
{Code: "lang_iw", Name: "עברית (Hebrew)"},
|
{Code: "iw", Name: "עברית (Hebrew)"},
|
||||||
{Code: "lang_hi", Name: "हिन्दी (Hindi)"},
|
{Code: "hi", Name: "हिन्दी (Hindi)"},
|
||||||
{Code: "lang_hu", Name: "magyar (Hungarian)"},
|
{Code: "hu", Name: "magyar (Hungarian)"},
|
||||||
{Code: "lang_is", Name: "íslenska (Icelandic)"},
|
{Code: "is", Name: "íslenska (Icelandic)"},
|
||||||
{Code: "lang_id", Name: "Bahasa Indonesia (Indonesian)"},
|
{Code: "id", Name: "Bahasa Indonesia (Indonesian)"},
|
||||||
{Code: "lang_it", Name: "italiano (Italian)"},
|
{Code: "it", Name: "italiano (Italian)"},
|
||||||
{Code: "lang_ja", Name: "日本語 (Japanese)"},
|
{Code: "ja", Name: "日本語 (Japanese)"},
|
||||||
{Code: "lang_ko", Name: "한국어 (Korean)"},
|
{Code: "ko", Name: "한국어 (Korean)"},
|
||||||
{Code: "lang_lv", Name: "latviešu (Latvian)"},
|
{Code: "lv", Name: "latviešu (Latvian)"},
|
||||||
{Code: "lang_lt", Name: "lietuvių (Lithuanian)"},
|
{Code: "lt", Name: "lietuvių (Lithuanian)"},
|
||||||
{Code: "lang_no", Name: "norsk (Norwegian)"},
|
{Code: "no", Name: "norsk (Norwegian)"},
|
||||||
{Code: "lang_fa", Name: "فارسی (Persian)"},
|
{Code: "fa", Name: "فارسی (Persian)"},
|
||||||
{Code: "lang_pl", Name: "polski (Polish)"},
|
{Code: "pl", Name: "polski (Polish)"},
|
||||||
{Code: "lang_pt", Name: "português (Portuguese)"},
|
{Code: "pt", Name: "português (Portuguese)"},
|
||||||
{Code: "lang_ro", Name: "română (Romanian)"},
|
{Code: "ro", Name: "română (Romanian)"},
|
||||||
{Code: "lang_ru", Name: "русский (Russian)"},
|
{Code: "ru", Name: "русский (Russian)"},
|
||||||
{Code: "lang_sr", Name: "српски (Serbian)"},
|
{Code: "sr", Name: "српски (Serbian)"},
|
||||||
{Code: "lang_sk", Name: "slovenčina (Slovak)"},
|
{Code: "sk", Name: "slovenčina (Slovak)"},
|
||||||
{Code: "lang_sl", Name: "slovenščina (Slovenian)"},
|
{Code: "sl", Name: "slovenščina (Slovenian)"},
|
||||||
{Code: "lang_es", Name: "español (Spanish)"},
|
{Code: "es", Name: "español (Spanish)"},
|
||||||
{Code: "lang_sw", Name: "Kiswahili (Swahili)"},
|
{Code: "sw", Name: "Kiswahili (Swahili)"},
|
||||||
{Code: "lang_sv", Name: "svenska (Swedish)"},
|
{Code: "sv", Name: "svenska (Swedish)"},
|
||||||
{Code: "lang_th", Name: "ไทย (Thai)"},
|
{Code: "th", Name: "ไทย (Thai)"},
|
||||||
{Code: "lang_tr", Name: "Türkçe (Turkish)"},
|
{Code: "tr", Name: "Türkçe (Turkish)"},
|
||||||
{Code: "lang_uk", Name: "українська (Ukrainian)"},
|
{Code: "uk", Name: "українська (Ukrainian)"},
|
||||||
{Code: "lang_vi", Name: "Tiếng Việt (Vietnamese)"},
|
{Code: "vi", Name: "Tiếng Việt (Vietnamese)"},
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleSearch(w http.ResponseWriter, r *http.Request) {
|
func handleSearch(w http.ResponseWriter, r *http.Request) {
|
||||||
query, safe, lang, searchType, page := parseSearchParams(r)
|
query, safe, lang, searchType, page := parseSearchParams(r)
|
||||||
|
|
||||||
// Load user settings
|
// Load user settings
|
||||||
settings = loadUserSettings(r)
|
settings = loadUserSettings(w, r)
|
||||||
|
|
||||||
// Update the theme, safe search, and language based on query parameters or use existing settings
|
// Update theme if provided, or use existing settings
|
||||||
theme := r.URL.Query().Get("theme")
|
theme := r.URL.Query().Get("theme")
|
||||||
if theme != "" {
|
if theme != "" {
|
||||||
settings.Theme = theme
|
settings.Theme = theme
|
||||||
saveUserSettings(w, settings)
|
saveUserSettings(w, settings) // Save if theme is updated
|
||||||
} else if settings.Theme == "" {
|
} else if settings.Theme == "" {
|
||||||
settings.Theme = "dark" // Default theme
|
settings.Theme = "dark" // Default theme
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update safe search if provided, or use existing settings
|
||||||
if safe != "" && safe != settings.SafeSearch {
|
if safe != "" && safe != settings.SafeSearch {
|
||||||
settings.SafeSearch = safe
|
settings.SafeSearch = safe
|
||||||
saveUserSettings(w, settings)
|
saveUserSettings(w, settings) // Save if safe search is updated
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update language if provided, or use existing settings
|
||||||
if lang != "" && lang != settings.Language {
|
if lang != "" && lang != settings.Language {
|
||||||
settings.Language = lang
|
settings.Language = lang
|
||||||
saveUserSettings(w, settings)
|
saveUserSettings(w, settings) // Save if language is updated
|
||||||
|
} else if settings.Language == "" {
|
||||||
|
// If no language set, auto-detect from browser or default to "en"
|
||||||
|
settings.Language = normalizeLangCode(r.Header.Get("Accept-Language"))
|
||||||
|
saveUserSettings(w, settings) // Save if language is auto-detected
|
||||||
|
}
|
||||||
|
|
||||||
|
// This will do for now (to handle Dark Reader addon)
|
||||||
|
switch settings.Theme {
|
||||||
|
case "dark", "black", "night", "latte":
|
||||||
|
settings.IsThemeDark = true
|
||||||
|
default:
|
||||||
|
settings.IsThemeDark = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if there is a search query
|
// Check if there is a search query
|
||||||
|
@ -99,11 +113,13 @@ func handleSearch(w http.ResponseWriter, r *http.Request) {
|
||||||
CurrentLang string
|
CurrentLang string
|
||||||
Theme string
|
Theme string
|
||||||
Safe string
|
Safe string
|
||||||
|
IsThemeDark bool
|
||||||
}{
|
}{
|
||||||
LanguageOptions: languageOptions,
|
LanguageOptions: languageOptions,
|
||||||
CurrentLang: settings.Language,
|
CurrentLang: settings.Language,
|
||||||
Theme: settings.Theme,
|
Theme: settings.Theme,
|
||||||
Safe: settings.SafeSearch,
|
Safe: settings.SafeSearch,
|
||||||
|
IsThemeDark: settings.IsThemeDark,
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpl := template.Must(template.ParseFiles("templates/search.html"))
|
tmpl := template.Must(template.ParseFiles("templates/search.html"))
|
||||||
|
@ -166,6 +182,7 @@ func runServer() {
|
||||||
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
|
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
|
||||||
http.HandleFunc("/", handleSearch)
|
http.HandleFunc("/", handleSearch)
|
||||||
http.HandleFunc("/search", handleSearch)
|
http.HandleFunc("/search", handleSearch)
|
||||||
|
http.HandleFunc("/suggestions", handleSuggestions)
|
||||||
http.HandleFunc("/img_proxy", handleImageProxy)
|
http.HandleFunc("/img_proxy", handleImageProxy)
|
||||||
http.HandleFunc("/node", handleNodeRequest)
|
http.HandleFunc("/node", handleNodeRequest)
|
||||||
http.HandleFunc("/settings", handleSettings)
|
http.HandleFunc("/settings", handleSettings)
|
||||||
|
|
|
@ -12,7 +12,7 @@ type OpenSearchDescription struct {
|
||||||
ShortName string `xml:"ShortName"`
|
ShortName string `xml:"ShortName"`
|
||||||
Description string `xml:"Description"`
|
Description string `xml:"Description"`
|
||||||
Tags string `xml:"Tags"`
|
Tags string `xml:"Tags"`
|
||||||
URL URL `xml:"Url"`
|
URLs []URL `xml:"Url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type URL struct {
|
type URL struct {
|
||||||
|
@ -25,13 +25,19 @@ func generateOpenSearchXML(config Config) {
|
||||||
|
|
||||||
opensearch := OpenSearchDescription{
|
opensearch := OpenSearchDescription{
|
||||||
Xmlns: "http://a9.com/-/spec/opensearch/1.1/",
|
Xmlns: "http://a9.com/-/spec/opensearch/1.1/",
|
||||||
ShortName: "Search Engine",
|
ShortName: "Warp",
|
||||||
Description: "Search engine",
|
Description: "Warp search engine",
|
||||||
Tags: "search, engine",
|
Tags: "search, engine, warp",
|
||||||
URL: URL{
|
URLs: []URL{
|
||||||
|
{
|
||||||
Type: "text/html",
|
Type: "text/html",
|
||||||
Template: fmt.Sprintf("%s/search?q={searchTerms}", baseURL),
|
Template: fmt.Sprintf("%s/search?q={searchTerms}", baseURL),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Type: "application/x-suggestions+json",
|
||||||
|
Template: fmt.Sprintf("%s/suggestions?q={searchTerms}", baseURL),
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
file, err := os.Create("static/opensearch.xml")
|
file, err := os.Create("static/opensearch.xml")
|
||||||
|
|
|
@ -1732,9 +1732,9 @@ body, h1, p, a, input, button {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-menu {
|
/* .search-menu {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
} */
|
||||||
|
|
||||||
.sub-search-button-wrapper {
|
.sub-search-button-wrapper {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
@ -1838,9 +1838,9 @@ body, h1, p, a, input, button {
|
||||||
top: 5px;
|
top: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-search-div {
|
/* .settings-search-div {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
} */
|
||||||
|
|
||||||
.search-container h1 {
|
.search-container h1 {
|
||||||
font-size: 55px;
|
font-size: 55px;
|
||||||
|
@ -1861,9 +1861,7 @@ body, h1, p, a, input, button {
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-button-wrapper button {
|
.search-button-wrapper button {
|
||||||
display: table-row;
|
display: none;
|
||||||
margin: 30px 0px 0px 0px;
|
|
||||||
width: 80%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#clearSearch {
|
#clearSearch {
|
||||||
|
|
188
static/js/autocomplete.js
Normal file
188
static/js/autocomplete.js
Normal file
|
@ -0,0 +1,188 @@
|
||||||
|
/**
|
||||||
|
* @source: ./script.js (originally from araa-search on Github)
|
||||||
|
*
|
||||||
|
* @licstart The following is the entire license notice for the
|
||||||
|
* JavaScript code in this page.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2023 Extravi
|
||||||
|
*
|
||||||
|
* The JavaScript code in this page is free software: you can
|
||||||
|
* redistribute it and/or modify it under the terms of the GNU Affero
|
||||||
|
* General Public License as published by the Free Software Foundation,
|
||||||
|
* either version 3 of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* The code is distributed WITHOUT ANY WARRANTY; without even the
|
||||||
|
* implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||||
|
* See the GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* As additional permission under GNU Affero General Public License
|
||||||
|
* section 7, you may distribute non-source (e.g., minimized or compacted)
|
||||||
|
* forms of that code without the copy of the GNU Affero General Public
|
||||||
|
* License normally required by section 4, provided you include this
|
||||||
|
* license notice and a URL through which recipients can access the
|
||||||
|
* Corresponding Source.
|
||||||
|
*
|
||||||
|
* @licend The above is the entire license notice
|
||||||
|
* for the JavaScript code in this page.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Removes the 'Apply Settings' button for Javascript users,
|
||||||
|
// since changing any of the elements causes the settings to apply
|
||||||
|
// automatically.
|
||||||
|
let resultsSave = document.querySelector(".results-save");
|
||||||
|
if (resultsSave != null) {
|
||||||
|
resultsSave.style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchInput = document.getElementById('search-input');
|
||||||
|
const searchWrapper = document.querySelectorAll('.wrapper, .wrapper-results')[0];
|
||||||
|
const resultsWrapper = document.querySelector('.autocomplete');
|
||||||
|
// const clearSearch = document.querySelector("#clearSearch");
|
||||||
|
|
||||||
|
async function getSuggestions(query) {
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams({ "q": query }).toString();
|
||||||
|
const response = await fetch(`/suggestions?${params}`);
|
||||||
|
const data = await response.json();
|
||||||
|
return data[1]; // Return only the array of suggestion strings
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentIndex = -1; // Keep track of the currently selected suggestion
|
||||||
|
|
||||||
|
let results = [];
|
||||||
|
searchInput.addEventListener('input', async () => {
|
||||||
|
let input = searchInput.value;
|
||||||
|
if (input.length) {
|
||||||
|
results = await getSuggestions(input);
|
||||||
|
}
|
||||||
|
renderResults(results);
|
||||||
|
currentIndex = -1; // Reset index when we return new results
|
||||||
|
});
|
||||||
|
|
||||||
|
searchInput.addEventListener("focus", async () => {
|
||||||
|
let input = searchInput.value;
|
||||||
|
if (results.length === 0 && input.length != 0) {
|
||||||
|
results = await getSuggestions(input);
|
||||||
|
}
|
||||||
|
renderResults(results);
|
||||||
|
})
|
||||||
|
|
||||||
|
// clearSearch.style.visibility = "visible"; // Only show the clear search button for JS users.
|
||||||
|
// clearSearch.addEventListener("click", () => {
|
||||||
|
// searchInput.value = "";
|
||||||
|
// searchInput.focus();
|
||||||
|
// })
|
||||||
|
|
||||||
|
searchInput.addEventListener('keydown', (event) => {
|
||||||
|
if (event.key === 'ArrowUp' || event.key === 'ArrowDown' || event.key === 'Tab') {
|
||||||
|
event.preventDefault(); // Prevent the default behavior, such as moving the cursor
|
||||||
|
|
||||||
|
// Find the currently selected suggestion element
|
||||||
|
const selectedSuggestion = resultsWrapper.querySelector('.selected');
|
||||||
|
if (selectedSuggestion) {
|
||||||
|
selectedSuggestion.classList.remove('selected'); // Deselect the currently selected suggestion
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment current index when ArrowUp is pressed otherwise hen Tab OR ArrowDown decrement
|
||||||
|
if (event.key === 'ArrowUp') {
|
||||||
|
currentIndex--;
|
||||||
|
} else {
|
||||||
|
currentIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap around the index if it goes out of bounds
|
||||||
|
if (currentIndex < 0) {
|
||||||
|
currentIndex = resultsWrapper.querySelectorAll('li').length - 1;
|
||||||
|
} else if (currentIndex >= resultsWrapper.querySelectorAll('li').length) {
|
||||||
|
currentIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select the new suggestion
|
||||||
|
resultsWrapper.querySelectorAll('li')[currentIndex].classList.add('selected');
|
||||||
|
// Update the value of the search input
|
||||||
|
searchInput.value = resultsWrapper.querySelectorAll('li')[currentIndex].textContent;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Default to the currently selected type or fallback to 'text'
|
||||||
|
let selectedType = document.querySelector('.search-active')?.value || 'text';
|
||||||
|
|
||||||
|
// Function to render results
|
||||||
|
function renderResults(results) {
|
||||||
|
if (!results || !results.length || !searchInput.value) {
|
||||||
|
searchWrapper.classList.remove('show');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = '';
|
||||||
|
results.forEach((item) => {
|
||||||
|
content += `<li>${item}</li>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (searchInput.value) {
|
||||||
|
searchWrapper.classList.add('show');
|
||||||
|
}
|
||||||
|
resultsWrapper.innerHTML = `<ul>${content}</ul>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to handle search input
|
||||||
|
searchInput.addEventListener('input', async () => {
|
||||||
|
let input = searchInput.value;
|
||||||
|
if (input.length) {
|
||||||
|
const results = await getSuggestions(input);
|
||||||
|
renderResults(results);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle click events on the type buttons
|
||||||
|
const typeButtons = document.querySelectorAll('[name="t"]');
|
||||||
|
typeButtons.forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
selectedType = this.value;
|
||||||
|
typeButtons.forEach(btn => btn.classList.remove('search-active'));
|
||||||
|
this.classList.add('search-active');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle clicks on search results
|
||||||
|
resultsWrapper.addEventListener('click', (event) => {
|
||||||
|
if (event.target.tagName === 'LI') {
|
||||||
|
const query = event.target.textContent;
|
||||||
|
window.location.href = `/search?q=${encodeURIComponent(query)}&t=${encodeURIComponent(selectedType)}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("keypress", (event) => {
|
||||||
|
if (document.activeElement == searchInput) {
|
||||||
|
// Allow the '/' character to be pressed when searchInput is active
|
||||||
|
} else if (document.querySelector(".calc") != null) {
|
||||||
|
// Do nothing if the calculator is available, so the division keybinding
|
||||||
|
// will still work
|
||||||
|
}
|
||||||
|
else if (event.key == "/") {
|
||||||
|
event.preventDefault();
|
||||||
|
searchInput.focus();
|
||||||
|
searchInput.selectionStart = searchInput.selectionEnd = searchInput.value.length;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add event listener to hide autocomplete suggestions when clicking outside of search-input or wrapper
|
||||||
|
document.addEventListener('click', (event) => {
|
||||||
|
// Check if the target of the event is the search-input or any of its ancestors
|
||||||
|
if (!searchInput.contains(event.target) && !searchWrapper.contains(event.target)) {
|
||||||
|
// Remove the show class from the search wrapper
|
||||||
|
searchWrapper.classList.remove('show');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update visual feedback for selected type on page load
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const activeButton = document.querySelector(`[name="t"][value="${selectedType}"]`);
|
||||||
|
if (activeButton) {
|
||||||
|
typeButtons.forEach(btn => btn.classList.remove('search-active'));
|
||||||
|
activeButton.classList.add('search-active');
|
||||||
|
}
|
||||||
|
});
|
96
static/js/dynamicscrolling.js
Normal file
96
static/js/dynamicscrolling.js
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
|
const templateData = document.getElementById('template-data');
|
||||||
|
let page = parseInt(templateData.getAttribute('data-page')) || 1;
|
||||||
|
const query = templateData.getAttribute('data-query') || '';
|
||||||
|
let searchType = templateData.getAttribute('data-type') || 'text'; // Default to 'text' if not provided
|
||||||
|
let loading = false;
|
||||||
|
let hasMoreResults = true;
|
||||||
|
const loadingIndicator = document.getElementById('message-bottom-left');
|
||||||
|
let loadingTimeout;
|
||||||
|
|
||||||
|
function loadResults(newPage) {
|
||||||
|
if (loading || !hasMoreResults) return;
|
||||||
|
loading = true;
|
||||||
|
|
||||||
|
// Show loading indicator if taking more than 100ms
|
||||||
|
loadingTimeout = setTimeout(() => {
|
||||||
|
loadingIndicator.style.display = 'flex';
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
fetch(`/search?q=${encodeURIComponent(query)}&t=${encodeURIComponent(searchType)}&p=${newPage}`)
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Network response was not ok');
|
||||||
|
}
|
||||||
|
return response.text();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
clearTimeout(loadingTimeout);
|
||||||
|
loadingIndicator.style.display = 'none';
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const doc = parser.parseFromString(data, 'text/html');
|
||||||
|
const newResults = doc.getElementById('results').innerHTML;
|
||||||
|
const noResultsMessage = `No results found for '${query}'. Try different keywords.`;
|
||||||
|
const endOfResultsMessage = "Looks like this is the end of results.";
|
||||||
|
const serverError = "Internal Server Error";
|
||||||
|
|
||||||
|
if (newResults.includes(noResultsMessage) || newResults.includes(endOfResultsMessage) || newResults.includes(serverError)) {
|
||||||
|
document.getElementById('results').innerHTML += newResults;
|
||||||
|
hasMoreResults = false;
|
||||||
|
} else {
|
||||||
|
document.getElementById('results').innerHTML += newResults;
|
||||||
|
page = newPage;
|
||||||
|
// Automatically load more results if content height is less than window height
|
||||||
|
checkIfMoreResultsNeeded();
|
||||||
|
}
|
||||||
|
loading = false;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
clearTimeout(loadingTimeout);
|
||||||
|
loadingIndicator.style.display = 'none';
|
||||||
|
console.error('Error loading results:', error);
|
||||||
|
hasMoreResults = false;
|
||||||
|
loading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkIfMoreResultsNeeded() {
|
||||||
|
if (document.body.scrollHeight <= window.innerHeight && hasMoreResults) {
|
||||||
|
loadResults(page + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('scroll', () => {
|
||||||
|
if (window.innerHeight + window.scrollY >= document.body.offsetHeight) {
|
||||||
|
loadResults(page + 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ensure the correct search button has the active class at load
|
||||||
|
const buttons = document.querySelectorAll('.search-container-results-btn button');
|
||||||
|
if (buttons.length === 0) {
|
||||||
|
console.error("No search buttons found");
|
||||||
|
} else {
|
||||||
|
buttons.forEach(btn => {
|
||||||
|
btn.addEventListener('click', function() {
|
||||||
|
const activeElement = document.querySelector('.search-container-results-btn .search-active');
|
||||||
|
if (activeElement) {
|
||||||
|
activeElement.classList.remove('search-active');
|
||||||
|
}
|
||||||
|
this.classList.add('search-active');
|
||||||
|
// Update search type when button is clicked
|
||||||
|
searchType = this.getAttribute('value');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ensure one button is active on page load
|
||||||
|
const initialActiveElement = document.querySelector('.search-container-results-btn .search-active');
|
||||||
|
if (!initialActiveElement) {
|
||||||
|
buttons[0].classList.add('search-active');
|
||||||
|
searchType = buttons[0].getAttribute('value'); // Set default search type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if more results are needed right after the initial content is loaded
|
||||||
|
checkIfMoreResultsNeeded();
|
||||||
|
});
|
160
suggestions.go
Normal file
160
suggestions.go
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
func handleSuggestions(w http.ResponseWriter, r *http.Request) {
|
||||||
|
query := r.URL.Query().Get("q")
|
||||||
|
if query == "" {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
fmt.Fprintf(w, `["",[]]`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define the fallback sequence with Google lower in the hierarchy
|
||||||
|
suggestionSources := []func(string) []string{
|
||||||
|
fetchDuckDuckGoSuggestions,
|
||||||
|
fetchEdgeSuggestions,
|
||||||
|
fetchBraveSuggestions,
|
||||||
|
fetchEcosiaSuggestions,
|
||||||
|
fetchQwantSuggestions,
|
||||||
|
fetchStartpageSuggestions,
|
||||||
|
// fetchGoogleSuggestions, // I advise against it, but you can use it if you want to
|
||||||
|
}
|
||||||
|
|
||||||
|
var suggestions []string
|
||||||
|
for _, fetchFunc := range suggestionSources {
|
||||||
|
suggestions = fetchFunc(query)
|
||||||
|
if len(suggestions) > 0 {
|
||||||
|
printDebug("Suggestions found using %T", fetchFunc)
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
printWarn("%T did not return any suggestions or failed.", fetchFunc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(suggestions) == 0 {
|
||||||
|
printErr("All suggestion services failed. Returning empty response.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the final suggestions as JSON
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
fmt.Fprintf(w, `["",%s]`, toJSONStringArray(suggestions))
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchGoogleSuggestions(query string) []string {
|
||||||
|
encodedQuery := url.QueryEscape(query)
|
||||||
|
url := fmt.Sprintf("http://suggestqueries.google.com/complete/search?client=firefox&q=%s", encodedQuery)
|
||||||
|
printDebug("Fetching suggestions from Google: %s", url)
|
||||||
|
return fetchSuggestionsFromURL(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchDuckDuckGoSuggestions(query string) []string {
|
||||||
|
encodedQuery := url.QueryEscape(query)
|
||||||
|
url := fmt.Sprintf("https://duckduckgo.com/ac/?q=%s&type=list", encodedQuery)
|
||||||
|
printDebug("Fetching suggestions from DuckDuckGo: %s", url)
|
||||||
|
return fetchSuggestionsFromURL(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchEdgeSuggestions(query string) []string {
|
||||||
|
encodedQuery := url.QueryEscape(query)
|
||||||
|
url := fmt.Sprintf("https://api.bing.com/osjson.aspx?query=%s", encodedQuery)
|
||||||
|
printDebug("Fetching suggestions from Edge (Bing): %s", url)
|
||||||
|
return fetchSuggestionsFromURL(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchBraveSuggestions(query string) []string {
|
||||||
|
encodedQuery := url.QueryEscape(query)
|
||||||
|
url := fmt.Sprintf("https://search.brave.com/api/suggest?q=%s", encodedQuery)
|
||||||
|
printDebug("Fetching suggestions from Brave: %s", url)
|
||||||
|
return fetchSuggestionsFromURL(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchEcosiaSuggestions(query string) []string {
|
||||||
|
encodedQuery := url.QueryEscape(query)
|
||||||
|
url := fmt.Sprintf("https://ac.ecosia.org/?q=%s&type=list", encodedQuery)
|
||||||
|
printDebug("Fetching suggestions from Ecosia: %s", url)
|
||||||
|
return fetchSuggestionsFromURL(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchQwantSuggestions(query string) []string {
|
||||||
|
encodedQuery := url.QueryEscape(query)
|
||||||
|
url := fmt.Sprintf("https://api.qwant.com/v3/suggest?q=%s", encodedQuery)
|
||||||
|
printDebug("Fetching suggestions from Qwant: %s", url)
|
||||||
|
return fetchSuggestionsFromURL(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchStartpageSuggestions(query string) []string {
|
||||||
|
encodedQuery := url.QueryEscape(query)
|
||||||
|
url := fmt.Sprintf("https://startpage.com/suggestions?q=%s", encodedQuery)
|
||||||
|
printDebug("Fetching suggestions from Startpage: %s", url)
|
||||||
|
return fetchSuggestionsFromURL(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchSuggestionsFromURL(url string) []string {
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
printWarn("Error fetching suggestions from %s: %v", url, err)
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
printWarn("Error reading response body from %s: %v", url, err)
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log the Content-Type for debugging
|
||||||
|
contentType := resp.Header.Get("Content-Type")
|
||||||
|
printDebug("Response Content-Type from %s: %s", url, contentType)
|
||||||
|
|
||||||
|
// Check if the body is non-empty
|
||||||
|
if len(body) == 0 {
|
||||||
|
printWarn("Received empty response body from %s", url)
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to parse the response as JSON regardless of Content-Type
|
||||||
|
var parsedResponse []interface{}
|
||||||
|
if err := json.Unmarshal(body, &parsedResponse); err != nil {
|
||||||
|
printErr("Error parsing JSON from %s: %v", url, err)
|
||||||
|
printDebug("Response body: %s", string(body))
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the response structure is as expected
|
||||||
|
if len(parsedResponse) < 2 {
|
||||||
|
printWarn("Unexpected response format from %v: %v", url, string(body))
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
suggestions := []string{}
|
||||||
|
if items, ok := parsedResponse[1].([]interface{}); ok {
|
||||||
|
for _, item := range items {
|
||||||
|
if suggestion, ok := item.(string); ok {
|
||||||
|
suggestions = append(suggestions, suggestion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
printErr("Unexpected suggestions format in response from: %v", url)
|
||||||
|
}
|
||||||
|
|
||||||
|
return suggestions
|
||||||
|
}
|
||||||
|
|
||||||
|
func toJSONStringArray(strings []string) string {
|
||||||
|
result := ""
|
||||||
|
for i, str := range strings {
|
||||||
|
result += fmt.Sprintf(`"%s"`, str)
|
||||||
|
if i < len(strings)-1 {
|
||||||
|
result += ","
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "[" + result + "]"
|
||||||
|
}
|
|
@ -3,6 +3,9 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
{{ if .IsThemeDark }}
|
||||||
|
<meta name="darkreader-lock">
|
||||||
|
{{ end }}
|
||||||
<title>{{.Query}} - Ocásek</title>
|
<title>{{.Query}} - Ocásek</title>
|
||||||
<link rel="stylesheet" href="/static/css/style.css">
|
<link rel="stylesheet" href="/static/css/style.css">
|
||||||
<link rel="stylesheet" href="/static/css/{{.Theme}}.css">
|
<link rel="stylesheet" href="/static/css/{{.Theme}}.css">
|
||||||
|
@ -14,6 +17,10 @@
|
||||||
<div class="wrapper-results">
|
<div class="wrapper-results">
|
||||||
<input type="text" name="q" value="{{ .Query }}" id="search-input" placeholder="Type to search..." />
|
<input type="text" name="q" value="{{ .Query }}" id="search-input" placeholder="Type to search..." />
|
||||||
<button id="search-wrapper-ico" class="material-icons-round" name="t" value="file">search</button>
|
<button id="search-wrapper-ico" class="material-icons-round" name="t" value="file">search</button>
|
||||||
|
<div class="autocomplete">
|
||||||
|
<ul>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
<input type="submit" class="hide" name="t" value="file" />
|
<input type="submit" class="hide" name="t" value="file" />
|
||||||
</div>
|
</div>
|
||||||
<div class="sub-search-button-wrapper">
|
<div class="sub-search-button-wrapper">
|
||||||
|
@ -105,6 +112,7 @@
|
||||||
Try rephrasing your search term and/or recorrect any spelling mistakes.
|
Try rephrasing your search term and/or recorrect any spelling mistakes.
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
<script defer src="/static/js/autocomplete.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// Check if JavaScript is enabled and modify the DOM accordingly
|
// Check if JavaScript is enabled and modify the DOM accordingly
|
||||||
document.getElementById('content').classList.remove('js-enabled');
|
document.getElementById('content').classList.remove('js-enabled');
|
||||||
|
|
|
@ -3,6 +3,9 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
{{ if .IsThemeDark }}
|
||||||
|
<meta name="darkreader-lock">
|
||||||
|
{{ end }}
|
||||||
<title>{{.Query}} - Ocásek</title>
|
<title>{{.Query}} - Ocásek</title>
|
||||||
<link rel="stylesheet" href="/static/css/style.css">
|
<link rel="stylesheet" href="/static/css/style.css">
|
||||||
<link rel="stylesheet" href="/static/css/{{.Theme}}.css">
|
<link rel="stylesheet" href="/static/css/{{.Theme}}.css">
|
||||||
|
@ -14,6 +17,9 @@
|
||||||
<div class="wrapper-results">
|
<div class="wrapper-results">
|
||||||
<input type="text" name="q" value="{{ .Query }}" id="search-input" placeholder="Type to search..." />
|
<input type="text" name="q" value="{{ .Query }}" id="search-input" placeholder="Type to search..." />
|
||||||
<button id="search-wrapper-ico" class="material-icons-round" name="t" value="forum">search</button>
|
<button id="search-wrapper-ico" class="material-icons-round" name="t" value="forum">search</button>
|
||||||
|
<div class="autocomplete">
|
||||||
|
<ul></ul>
|
||||||
|
</div>
|
||||||
<input type="submit" class="hide" name="t" value="forum" />
|
<input type="submit" class="hide" name="t" value="forum" />
|
||||||
</div>
|
</div>
|
||||||
<div class="sub-search-button-wrapper">
|
<div class="sub-search-button-wrapper">
|
||||||
|
@ -56,9 +62,9 @@
|
||||||
<option value="{{.Code}}" {{if eq .Code $.CurrentLang}}selected{{end}}>{{.Name}}</option>
|
<option value="{{.Code}}" {{if eq .Code $.CurrentLang}}selected{{end}}>{{.Name}}</option>
|
||||||
{{end}}
|
{{end}}
|
||||||
</select>
|
</select>
|
||||||
<button class="results-save" name="t" value="text">Apply settings</button>
|
<button class="results-save" name="t" value="forum">Apply settings</button>
|
||||||
</form>
|
</form>
|
||||||
<div class="results">
|
<div class="results" id="results">
|
||||||
{{if .Results}}
|
{{if .Results}}
|
||||||
{{range .Results}}
|
{{range .Results}}
|
||||||
<div class="result_item">
|
<div class="result_item">
|
||||||
|
@ -68,22 +74,33 @@
|
||||||
</div>
|
</div>
|
||||||
<br>
|
<br>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{else}}
|
{{else if .NoResults}}
|
||||||
<div class="no-results">No results found for '{{ .Query }}'. Try different keywords.</div>
|
<div class="no-results">No results found for '{{ .Query }}'. Try different keywords.</div>
|
||||||
|
{{else}}
|
||||||
|
<div class="no-more-results">Looks like this is the end of results.</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
<div class="prev-next prev-img">
|
<div class="message-bottom-left" id="message-bottom-left">
|
||||||
|
<span>Searching for new results...</span>
|
||||||
|
</div>
|
||||||
|
<div class="prev-next prev-img" id="prev-next">
|
||||||
<form action="/search" method="get">
|
<form action="/search" method="get">
|
||||||
<input type="hidden" name="q" value="{{ .Query }}">
|
<input type="hidden" name="q" value="{{ .Query }}">
|
||||||
<input type="hidden" name="t" value="text">
|
<input type="hidden" name="t" value="forum">
|
||||||
|
<div id="content" class="js-enabled">
|
||||||
{{ if .HasPrevPage }}
|
{{ if .HasPrevPage }}
|
||||||
<button type="submit" name="p" value="{{ sub .Page 1 }}">Previous</button>
|
<button type="submit" name="p" value="{{ sub .Page 1 }}">Previous</button>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ if .HasNextPage }}
|
{{ if .HasNextPage }}
|
||||||
<button type="submit" name="p" value="{{ add .Page 1 }}">Next</button>
|
<button type="submit" name="p" value="{{ add .Page 1 }}">Next</button>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Pass the relevant data for dynamic scrolling -->
|
||||||
|
<div id="template-data" data-page="{{ .Page }}" data-query="{{ .Query }}" data-type="forum"></div>
|
||||||
|
<script defer src="/static/js/dynamicscrolling.js"></script>
|
||||||
|
<script defer src="/static/js/autocomplete.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// Check if JavaScript is enabled and modify the DOM accordingly
|
// Check if JavaScript is enabled and modify the DOM accordingly
|
||||||
document.getElementById('content').classList.remove('js-enabled');
|
document.getElementById('content').classList.remove('js-enabled');
|
||||||
|
|
|
@ -3,6 +3,9 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
{{ if .IsThemeDark }}
|
||||||
|
<meta name="darkreader-lock">
|
||||||
|
{{ end }}
|
||||||
<title>{{.Query}} - Ocásek</title>
|
<title>{{.Query}} - Ocásek</title>
|
||||||
<link rel="stylesheet" href="/static/css/style.css">
|
<link rel="stylesheet" href="/static/css/style.css">
|
||||||
<link rel="stylesheet" href="/static/css/{{.Theme}}.css">
|
<link rel="stylesheet" href="/static/css/{{.Theme}}.css">
|
||||||
|
@ -14,6 +17,10 @@
|
||||||
<div class="wrapper-results">
|
<div class="wrapper-results">
|
||||||
<input type="text" name="q" value="{{ .Query }}" id="search-input" placeholder="Type to search..." />
|
<input type="text" name="q" value="{{ .Query }}" id="search-input" placeholder="Type to search..." />
|
||||||
<button id="search-wrapper-ico" class="material-icons-round" name="t" value="image">search</button>
|
<button id="search-wrapper-ico" class="material-icons-round" name="t" value="image">search</button>
|
||||||
|
<div class="autocomplete">
|
||||||
|
<ul>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
<input type="submit" class="hide" name="t" value="image" />
|
<input type="submit" class="hide" name="t" value="image" />
|
||||||
</div>
|
</div>
|
||||||
<div class="sub-search-button-wrapper">
|
<div class="sub-search-button-wrapper">
|
||||||
|
@ -101,69 +108,12 @@
|
||||||
<div class="message-bottom-left" id="message-bottom-left">
|
<div class="message-bottom-left" id="message-bottom-left">
|
||||||
<span>Searching for new results...</span>
|
<span>Searching for new results...</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="template-data" data-page="{{ .Page }}" data-query="{{ .Query }}" data-type="image"></div>
|
||||||
|
<script defer src="/static/js/dynamicscrolling.js"></script>
|
||||||
|
<script defer src="/static/js/autocomplete.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// Check if JavaScript is enabled and modify the DOM accordingly
|
// Check if JavaScript is enabled and modify the DOM accordingly
|
||||||
document.getElementById('content').classList.remove('js-enabled');
|
document.getElementById('content').classList.remove('js-enabled');
|
||||||
</script>
|
</script>
|
||||||
<script>
|
|
||||||
document.addEventListener("DOMContentLoaded", function() {
|
|
||||||
let page = {{ .Page }};
|
|
||||||
const query = "{{ .Query }}";
|
|
||||||
let loading = false;
|
|
||||||
let hasMoreResults = true;
|
|
||||||
const loadingIndicator = document.getElementById('message-bottom-left');
|
|
||||||
let loadingTimeout;
|
|
||||||
|
|
||||||
function loadResults(newPage) {
|
|
||||||
if (loading || !hasMoreResults) return;
|
|
||||||
loading = true;
|
|
||||||
|
|
||||||
// Show loading indicator if taking more than 100ms
|
|
||||||
loadingTimeout = setTimeout(() => {
|
|
||||||
loadingIndicator.style.display = 'flex';
|
|
||||||
}, 100);
|
|
||||||
|
|
||||||
fetch(`/search?q=${encodeURIComponent(query)}&t=image&p=${newPage}`)
|
|
||||||
.then(response => {
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Network response was not ok');
|
|
||||||
}
|
|
||||||
return response.text();
|
|
||||||
})
|
|
||||||
.then(data => {
|
|
||||||
clearTimeout(loadingTimeout);
|
|
||||||
loadingIndicator.style.display = 'none';
|
|
||||||
const parser = new DOMParser();
|
|
||||||
const doc = parser.parseFromString(data, 'text/html');
|
|
||||||
const newResults = doc.getElementById('results').innerHTML;
|
|
||||||
const noResultsMessage = "No results found for '{{ .Query }}'. Try different keywords.";
|
|
||||||
const endOfResultsMessage = "Looks like this is the end of results.";
|
|
||||||
const serverError = "Internal Server Error";
|
|
||||||
|
|
||||||
if (newResults.includes(noResultsMessage) || newResults.includes(endOfResultsMessage) || newResults.includes(serverError)) {
|
|
||||||
document.getElementById('results').innerHTML += newResults;
|
|
||||||
hasMoreResults = false;
|
|
||||||
} else {
|
|
||||||
document.getElementById('results').innerHTML += newResults;
|
|
||||||
page = newPage;
|
|
||||||
}
|
|
||||||
loading = false;
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
clearTimeout(loadingTimeout);
|
|
||||||
loadingIndicator.style.display = 'none';
|
|
||||||
console.error('Error loading results:', error);
|
|
||||||
hasMoreResults = false; // Stop further attempts
|
|
||||||
loading = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('scroll', () => {
|
|
||||||
if (window.innerHeight + window.scrollY >= document.body.offsetHeight) {
|
|
||||||
loadResults(page + 1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -3,6 +3,9 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
{{ if .IsThemeDark }}
|
||||||
|
<meta name="darkreader-lock">
|
||||||
|
{{ end }}
|
||||||
<title>{{ .Query }} - Ocásek</title>
|
<title>{{ .Query }} - Ocásek</title>
|
||||||
<link rel="stylesheet" href="/static/css/style.css">
|
<link rel="stylesheet" href="/static/css/style.css">
|
||||||
<link rel="stylesheet" href="/static/css/{{.Theme}}.css">
|
<link rel="stylesheet" href="/static/css/{{.Theme}}.css">
|
||||||
|
@ -29,6 +32,10 @@
|
||||||
<div class="wrapper-results">
|
<div class="wrapper-results">
|
||||||
<input type="text" name="q" value="{{ .Query }}" id="search-input" placeholder="Type to search..." />
|
<input type="text" name="q" value="{{ .Query }}" id="search-input" placeholder="Type to search..." />
|
||||||
<button id="search-wrapper-ico" class="material-icons-round" name="t" value="map">search</button>
|
<button id="search-wrapper-ico" class="material-icons-round" name="t" value="map">search</button>
|
||||||
|
<div class="autocomplete">
|
||||||
|
<ul>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
<input type="submit" class="hide" name="t" value="map" />
|
<input type="submit" class="hide" name="t" value="map" />
|
||||||
</div>
|
</div>
|
||||||
<div class="sub-search-button-wrapper">
|
<div class="sub-search-button-wrapper">
|
||||||
|
|
|
@ -3,12 +3,16 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
{{ if .IsThemeDark }}
|
||||||
|
<meta name="darkreader-lock">
|
||||||
|
{{ end }}
|
||||||
<title>Search with Ocásek</title>
|
<title>Search with Ocásek</title>
|
||||||
<link rel="stylesheet" href="/static/css/style.css">
|
<link rel="stylesheet" href="/static/css/style.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="Ocásek" href="/opensearch.xml">
|
<link rel="search" type="application/opensearchdescription+xml" title="Ocásek" href="/opensearch.xml">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<script defer src="/static/js/autocomplete.js"></script>
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
// Define the updateSettings function first
|
// Define the updateSettings function first
|
||||||
|
@ -83,6 +87,10 @@
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<input type="text" name="q" autofocus id="search-input" placeholder="Type to search..." />
|
<input type="text" name="q" autofocus id="search-input" placeholder="Type to search..." />
|
||||||
<button id="search-wrapper-ico" class="material-icons-round" name="t" value="text" type="submit">search</button>
|
<button id="search-wrapper-ico" class="material-icons-round" name="t" value="text" type="submit">search</button>
|
||||||
|
<div class="autocomplete">
|
||||||
|
<ul>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
<!-- <a id="clearSearch" class="material-icons-round">close</a> -->
|
<!-- <a id="clearSearch" class="material-icons-round">close</a> -->
|
||||||
</div>
|
</div>
|
||||||
<div class="search-button-wrapper">
|
<div class="search-button-wrapper">
|
||||||
|
|
|
@ -3,6 +3,9 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
{{ if .IsThemeDark }}
|
||||||
|
<meta name="darkreader-lock">
|
||||||
|
{{ end }}
|
||||||
<title>Settings - Ocásek</title>
|
<title>Settings - Ocásek</title>
|
||||||
<link rel="stylesheet" href="/static/css/style.css">
|
<link rel="stylesheet" href="/static/css/style.css">
|
||||||
<link rel="stylesheet" href="/static/css/{{.Theme}}.css">
|
<link rel="stylesheet" href="/static/css/{{.Theme}}.css">
|
||||||
|
|
|
@ -3,6 +3,9 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
{{ if .IsThemeDark }}
|
||||||
|
<meta name="darkreader-lock">
|
||||||
|
{{ end }}
|
||||||
<title>{{.Query}} - Ocásek</title>
|
<title>{{.Query}} - Ocásek</title>
|
||||||
<link rel="stylesheet" href="/static/css/style.css">
|
<link rel="stylesheet" href="/static/css/style.css">
|
||||||
<link rel="stylesheet" href="/static/css/{{.Theme}}.css">
|
<link rel="stylesheet" href="/static/css/{{.Theme}}.css">
|
||||||
|
@ -14,6 +17,10 @@
|
||||||
<div class="wrapper-results">
|
<div class="wrapper-results">
|
||||||
<input type="text" name="q" value="{{ .Query }}" id="search-input" placeholder="Type to search..." />
|
<input type="text" name="q" value="{{ .Query }}" id="search-input" placeholder="Type to search..." />
|
||||||
<button id="search-wrapper-ico" class="material-icons-round" name="t" value="text">search</button>
|
<button id="search-wrapper-ico" class="material-icons-round" name="t" value="text">search</button>
|
||||||
|
<div class="autocomplete">
|
||||||
|
<ul>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
<input type="submit" class="hide" name="t" value="text" />
|
<input type="submit" class="hide" name="t" value="text" />
|
||||||
</div>
|
</div>
|
||||||
<div class="sub-search-button-wrapper">
|
<div class="sub-search-button-wrapper">
|
||||||
|
@ -81,73 +88,22 @@
|
||||||
<form action="/search" method="get">
|
<form action="/search" method="get">
|
||||||
<input type="hidden" name="q" value="{{ .Query }}">
|
<input type="hidden" name="q" value="{{ .Query }}">
|
||||||
<input type="hidden" name="t" value="text">
|
<input type="hidden" name="t" value="text">
|
||||||
|
<div id="content" class="js-enabled">
|
||||||
{{ if .HasPrevPage }}
|
{{ if .HasPrevPage }}
|
||||||
<button type="submit" name="p" value="{{ sub .Page 1 }}">Previous</button>
|
<button type="submit" name="p" value="{{ sub .Page 1 }}">Previous</button>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ if .HasNextPage }}
|
{{ if .HasNextPage }}
|
||||||
<button type="submit" name="p" value="{{ add .Page 1 }}">Next</button>
|
<button type="submit" name="p" value="{{ add .Page 1 }}">Next</button>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="template-data" data-page="{{ .Page }}" data-query="{{ .Query }}" data-type="text"></div>
|
||||||
|
<script defer src="/static/js/dynamicscrolling.js"></script>
|
||||||
|
<script defer src="/static/js/autocomplete.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// Check if JavaScript is enabled and modify the DOM accordingly
|
// Check if JavaScript is enabled and modify the DOM accordingly
|
||||||
document.getElementById('content').classList.remove('js-enabled');
|
document.getElementById('content').classList.remove('js-enabled');
|
||||||
</script>
|
</script>
|
||||||
<script>
|
|
||||||
document.addEventListener("DOMContentLoaded", function() {
|
|
||||||
if (document.getElementById('prev-next')) {
|
|
||||||
document.getElementById('prev-next').style.display = 'none';
|
|
||||||
|
|
||||||
let page = {{ .Page }};
|
|
||||||
const query = "{{ .Query }}";
|
|
||||||
let loading = false;
|
|
||||||
let hasMoreResults = true;
|
|
||||||
const loadingIndicator = document.getElementById('message-bottom-left');
|
|
||||||
let loadingTimeout;
|
|
||||||
|
|
||||||
function loadResults(newPage) {
|
|
||||||
if (loading || !hasMoreResults) return;
|
|
||||||
loading = true;
|
|
||||||
|
|
||||||
// Show loading indicator if taking more than 100ms
|
|
||||||
loadingTimeout = setTimeout(() => {
|
|
||||||
loadingIndicator.style.display = 'flex';
|
|
||||||
}, 100);
|
|
||||||
|
|
||||||
fetch(`/search?q=${encodeURIComponent(query)}&t=text&p=${newPage}`)
|
|
||||||
.then(response => response.text())
|
|
||||||
.then(data => {
|
|
||||||
clearTimeout(loadingTimeout);
|
|
||||||
loadingIndicator.style.display = 'none';
|
|
||||||
const parser = new DOMParser();
|
|
||||||
const doc = parser.parseFromString(data, 'text/html');
|
|
||||||
const newResults = doc.getElementById('results').innerHTML;
|
|
||||||
const noResultsMessage = "No results found for '{{ .Query }}'. Try different keywords.";
|
|
||||||
|
|
||||||
if (newResults.includes(noResultsMessage)) {
|
|
||||||
document.getElementById('results').innerHTML += "<div class='no-more-results'>Looks like this is the end of results.</div>";
|
|
||||||
hasMoreResults = false;
|
|
||||||
} else {
|
|
||||||
document.getElementById('results').innerHTML += newResults;
|
|
||||||
page = newPage;
|
|
||||||
}
|
|
||||||
loading = false;
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
clearTimeout(loadingTimeout);
|
|
||||||
loadingIndicator.style.display = 'none';
|
|
||||||
console.error('Error loading results:', error);
|
|
||||||
loading = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('scroll', () => {
|
|
||||||
if (window.innerHeight + window.scrollY >= document.body.offsetHeight) {
|
|
||||||
loadResults(page + 1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -3,6 +3,9 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
{{ if .IsThemeDark }}
|
||||||
|
<meta name="darkreader-lock">
|
||||||
|
{{ end }}
|
||||||
<title>{{.Query}} - Ocásek</title>
|
<title>{{.Query}} - Ocásek</title>
|
||||||
<link rel="stylesheet" href="/static/css/style.css">
|
<link rel="stylesheet" href="/static/css/style.css">
|
||||||
<link rel="stylesheet" href="/static/css/{{.Theme}}.css">
|
<link rel="stylesheet" href="/static/css/{{.Theme}}.css">
|
||||||
|
@ -14,6 +17,10 @@
|
||||||
<div class="wrapper-results">
|
<div class="wrapper-results">
|
||||||
<input type="text" name="q" value="{{ .Query }}" id="search-input" placeholder="Type to search..." />
|
<input type="text" name="q" value="{{ .Query }}" id="search-input" placeholder="Type to search..." />
|
||||||
<button id="search-wrapper-ico" class="material-icons-round" name="t" value="video">search</button>
|
<button id="search-wrapper-ico" class="material-icons-round" name="t" value="video">search</button>
|
||||||
|
<div class="autocomplete">
|
||||||
|
<ul>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
<input type="submit" class="hide" name="t" value="video" />
|
<input type="submit" class="hide" name="t" value="video" />
|
||||||
</div>
|
</div>
|
||||||
<div class="sub-search-button-wrapper">
|
<div class="sub-search-button-wrapper">
|
||||||
|
@ -82,6 +89,7 @@
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
<script defer src="/static/js/autocomplete.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// Check if JavaScript is enabled and modify the DOM accordingly
|
// Check if JavaScript is enabled and modify the DOM accordingly
|
||||||
document.getElementById('content').classList.remove('js-enabled');
|
document.getElementById('content').classList.remove('js-enabled');
|
||||||
|
|
|
@ -67,7 +67,7 @@ func buildSearchURL(query, safe, lang string, page, resultsPerPage int) string {
|
||||||
|
|
||||||
langParam := ""
|
langParam := ""
|
||||||
if lang != "" {
|
if lang != "" {
|
||||||
langParam = "&lr=" + lang
|
langParam = "&lr=lang_" + lang
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate random geolocation
|
// Generate random geolocation
|
||||||
|
|
2
text.go
2
text.go
|
@ -55,6 +55,7 @@ func HandleTextSearch(w http.ResponseWriter, settings UserSettings, query string
|
||||||
CurrentLang string
|
CurrentLang string
|
||||||
Theme string
|
Theme string
|
||||||
Safe string
|
Safe string
|
||||||
|
IsThemeDark bool
|
||||||
}{
|
}{
|
||||||
Results: combinedResults,
|
Results: combinedResults,
|
||||||
Query: query,
|
Query: query,
|
||||||
|
@ -67,6 +68,7 @@ func HandleTextSearch(w http.ResponseWriter, settings UserSettings, query string
|
||||||
CurrentLang: settings.Language,
|
CurrentLang: settings.Language,
|
||||||
Theme: settings.Theme,
|
Theme: settings.Theme,
|
||||||
Safe: settings.SafeSearch,
|
Safe: settings.SafeSearch,
|
||||||
|
IsThemeDark: settings.IsThemeDark,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = tmpl.Execute(w, data)
|
err = tmpl.Execute(w, data)
|
||||||
|
|
|
@ -3,6 +3,7 @@ package main
|
||||||
import (
|
import (
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -10,23 +11,38 @@ type UserSettings struct {
|
||||||
Theme string
|
Theme string
|
||||||
Language string
|
Language string
|
||||||
SafeSearch string
|
SafeSearch string
|
||||||
|
IsThemeDark bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadUserSettings(r *http.Request) UserSettings {
|
func loadUserSettings(w http.ResponseWriter, r *http.Request) UserSettings {
|
||||||
var settings UserSettings
|
var settings UserSettings
|
||||||
|
saveRequired := false // Track if we need to save settings back
|
||||||
|
|
||||||
// Load theme
|
// Load theme
|
||||||
if cookie, err := r.Cookie("theme"); err == nil {
|
if cookie, err := r.Cookie("theme"); err == nil {
|
||||||
settings.Theme = cookie.Value
|
settings.Theme = cookie.Value
|
||||||
} else {
|
} else {
|
||||||
settings.Theme = "dark" // Default theme
|
settings.Theme = "dark" // Default theme
|
||||||
|
saveRequired = true // No cookie found, need to save this later
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load language
|
// Load language
|
||||||
if cookie, err := r.Cookie("language"); err == nil {
|
if cookie, err := r.Cookie("language"); err == nil {
|
||||||
settings.Language = cookie.Value
|
settings.Language = cookie.Value
|
||||||
} else {
|
} else {
|
||||||
settings.Language = "en" // Default language
|
settings.Language = "" // Set language to empty, handled later
|
||||||
|
}
|
||||||
|
|
||||||
|
// If language is empty, get it from the Accept-Language header
|
||||||
|
if settings.Language == "" {
|
||||||
|
acceptLang := r.Header.Get("Accept-Language")
|
||||||
|
if acceptLang != "" {
|
||||||
|
// Get the first language from Accept-Language header and normalize
|
||||||
|
settings.Language = normalizeLangCode(strings.Split(acceptLang, ",")[0])
|
||||||
|
} else {
|
||||||
|
settings.Language = "en" // Default language if Accept-Language is not present
|
||||||
|
}
|
||||||
|
saveRequired = true // No language cookie found, need to save
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load safe search
|
// Load safe search
|
||||||
|
@ -34,6 +50,12 @@ func loadUserSettings(r *http.Request) UserSettings {
|
||||||
settings.SafeSearch = cookie.Value
|
settings.SafeSearch = cookie.Value
|
||||||
} else {
|
} else {
|
||||||
settings.SafeSearch = "" // Default safe search off
|
settings.SafeSearch = "" // Default safe search off
|
||||||
|
saveRequired = true // No safe search cookie found, need to save
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save settings if required (no cookie found for any of the settings)
|
||||||
|
if saveRequired {
|
||||||
|
saveUserSettings(w, settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
return settings
|
return settings
|
||||||
|
@ -73,7 +95,7 @@ func saveUserSettings(w http.ResponseWriter, settings UserSettings) {
|
||||||
func handleSaveSettings(w http.ResponseWriter, r *http.Request) {
|
func handleSaveSettings(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method == "POST" {
|
if r.Method == "POST" {
|
||||||
// Load current settings
|
// Load current settings
|
||||||
settings := loadUserSettings(r)
|
settings := loadUserSettings(w, r)
|
||||||
|
|
||||||
// Update only the settings that were submitted in the form
|
// Update only the settings that were submitted in the form
|
||||||
if theme := r.FormValue("theme"); theme != "" {
|
if theme := r.FormValue("theme"); theme != "" {
|
||||||
|
@ -81,6 +103,12 @@ func handleSaveSettings(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
if lang := r.FormValue("lang"); lang != "" {
|
if lang := r.FormValue("lang"); lang != "" {
|
||||||
settings.Language = lang
|
settings.Language = lang
|
||||||
|
} else {
|
||||||
|
// If lang is empty, try to get from Accept-Language header
|
||||||
|
acceptLang := r.Header.Get("Accept-Language")
|
||||||
|
if acceptLang != "" {
|
||||||
|
settings.Language = strings.Split(acceptLang, ",")[0]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if safe := r.FormValue("safe"); safe != "" {
|
if safe := r.FormValue("safe"); safe != "" {
|
||||||
settings.SafeSearch = safe
|
settings.SafeSearch = safe
|
||||||
|
@ -96,18 +124,20 @@ func handleSaveSettings(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
func handleSettings(w http.ResponseWriter, r *http.Request) {
|
func handleSettings(w http.ResponseWriter, r *http.Request) {
|
||||||
// Load user settings
|
// Load user settings
|
||||||
settings = loadUserSettings(r)
|
settings = loadUserSettings(w, r)
|
||||||
|
|
||||||
data := struct {
|
data := struct {
|
||||||
LanguageOptions []LanguageOption
|
LanguageOptions []LanguageOption
|
||||||
CurrentLang string
|
CurrentLang string
|
||||||
Theme string
|
Theme string
|
||||||
Safe string
|
Safe string
|
||||||
|
IsThemeDark bool
|
||||||
}{
|
}{
|
||||||
LanguageOptions: languageOptions,
|
LanguageOptions: languageOptions,
|
||||||
CurrentLang: settings.Language,
|
CurrentLang: settings.Language,
|
||||||
Theme: settings.Theme,
|
Theme: settings.Theme,
|
||||||
Safe: settings.SafeSearch,
|
Safe: settings.SafeSearch,
|
||||||
|
IsThemeDark: settings.IsThemeDark,
|
||||||
}
|
}
|
||||||
|
|
||||||
printDebug("Rendering settings with data: %+v", data)
|
printDebug("Rendering settings with data: %+v", data)
|
||||||
|
@ -126,3 +156,36 @@ func handleSettings(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function to normalize language codes
|
||||||
|
func normalizeLangCode(lang string) string {
|
||||||
|
lang = strings.ToLower(lang)
|
||||||
|
|
||||||
|
// First, check if the language code is already valid
|
||||||
|
if isValidLangCode(lang) {
|
||||||
|
return lang
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip regional codes (e.g., en-US -> en)
|
||||||
|
if strings.Contains(lang, "-") {
|
||||||
|
lang = strings.Split(lang, "-")[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-check if the normalized version is valid
|
||||||
|
if isValidLangCode(lang) {
|
||||||
|
return lang
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the language is not recognized, default to "en"
|
||||||
|
return "en"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to check if a language code exists in the language options
|
||||||
|
func isValidLangCode(lang string) bool {
|
||||||
|
for _, opt := range languageOptions {
|
||||||
|
if opt.Code == lang {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
1
video.go
1
video.go
|
@ -176,6 +176,7 @@ func handleVideoSearch(w http.ResponseWriter, settings UserSettings, query strin
|
||||||
"CurrentLang": settings.Language,
|
"CurrentLang": settings.Language,
|
||||||
"Theme": settings.Theme,
|
"Theme": settings.Theme,
|
||||||
"Safe": settings.SafeSearch,
|
"Safe": settings.SafeSearch,
|
||||||
|
"IsThemeDark": settings.IsThemeDark,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
printErr("Error executing template: %v", err)
|
printErr("Error executing template: %v", err)
|
||||||
|
|
Loading…
Reference in a new issue