This commit is contained in:
parent
c33a997dc5
commit
57507756ec
14 changed files with 864 additions and 97 deletions
|
@ -16,7 +16,6 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
@ -45,9 +44,6 @@ var (
|
||||||
"/apple-touch-icon.png",
|
"/apple-touch-icon.png",
|
||||||
"/apple-touch-icon-precomposed.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=["']([^"']+)["']`)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Add this near the top with other vars
|
// Add this near the top with other vars
|
||||||
|
@ -372,7 +368,7 @@ func cacheFavicon(imageURL, imageID string) (string, bool, error) {
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// Debug
|
// Debug
|
||||||
fmt.Printf("Downloading favicon [%s] for ID [%s]\n", imageURL, imageID)
|
printDebug("Downloading favicon ID: %s\n", imageID)
|
||||||
|
|
||||||
filename := fmt.Sprintf("%s_icon.webp", imageID)
|
filename := fmt.Sprintf("%s_icon.webp", imageID)
|
||||||
imageCacheDir := filepath.Join(config.DriveCache.Path, "images")
|
imageCacheDir := filepath.Join(config.DriveCache.Path, "images")
|
||||||
|
|
82
ia-calc.go
Normal file
82
ia-calc.go
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Enhanced math expression parser
|
||||||
|
func parseMathExpression(query string) (string, bool) {
|
||||||
|
// Clean and normalize the expression
|
||||||
|
query = strings.ReplaceAll(query, " ", "")
|
||||||
|
query = strings.ReplaceAll(query, ",", "")
|
||||||
|
|
||||||
|
// Regex to match valid math expressions
|
||||||
|
mathRegex := regexp.MustCompile(`^\d+(\.\d+)?([\+\-\*/\^]\d+(\.\d+)?)+$`)
|
||||||
|
if !mathRegex.MatchString(query) {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Operator precedence handling
|
||||||
|
operators := []struct {
|
||||||
|
symbol string
|
||||||
|
apply func(float64, float64) float64
|
||||||
|
}{
|
||||||
|
{"^", func(a, b float64) float64 {
|
||||||
|
result := 1.0
|
||||||
|
for i := 0; i < int(b); i++ {
|
||||||
|
result *= a
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}},
|
||||||
|
{"*", func(a, b float64) float64 { return a * b }},
|
||||||
|
{"/", func(a, b float64) float64 { return a / b }},
|
||||||
|
{"+", func(a, b float64) float64 { return a + b }},
|
||||||
|
{"-", func(a, b float64) float64 { return a - b }},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse numbers and operators
|
||||||
|
var tokens []interface{}
|
||||||
|
current := ""
|
||||||
|
for _, char := range query {
|
||||||
|
if char >= '0' && char <= '9' || char == '.' {
|
||||||
|
current += string(char)
|
||||||
|
} else {
|
||||||
|
if current != "" {
|
||||||
|
num, _ := strconv.ParseFloat(current, 64)
|
||||||
|
tokens = append(tokens, num)
|
||||||
|
current = ""
|
||||||
|
}
|
||||||
|
tokens = append(tokens, string(char))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if current != "" {
|
||||||
|
num, _ := strconv.ParseFloat(current, 64)
|
||||||
|
tokens = append(tokens, num)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evaluate expression with operator precedence
|
||||||
|
for _, op := range operators {
|
||||||
|
for i := 1; i < len(tokens)-1; i += 2 {
|
||||||
|
if operator, ok := tokens[i].(string); ok && operator == op.symbol {
|
||||||
|
left := tokens[i-1].(float64)
|
||||||
|
right := tokens[i+1].(float64)
|
||||||
|
result := op.apply(left, right)
|
||||||
|
|
||||||
|
// Update tokens
|
||||||
|
tokens = append(tokens[:i-1], tokens[i+2:]...)
|
||||||
|
tokens = append(tokens[:i-1], append([]interface{}{result}, tokens[i-1:]...)...)
|
||||||
|
i -= 2 // Adjust index after modification
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format result
|
||||||
|
result := tokens[0].(float64)
|
||||||
|
if result == float64(int(result)) {
|
||||||
|
return fmt.Sprintf("%d", int(result)), true
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%.2f", result), true
|
||||||
|
}
|
206
ia-currency.go
Normal file
206
ia-currency.go
Normal file
|
@ -0,0 +1,206 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ExchangeRateCache holds currency rates with automatic refresh
|
||||||
|
var (
|
||||||
|
exchangeRates = make(map[string]float64)
|
||||||
|
lastUpdated time.Time
|
||||||
|
exchangeCacheMutex sync.RWMutex
|
||||||
|
cacheExpiration = 1 * time.Hour
|
||||||
|
allCurrencies []string
|
||||||
|
)
|
||||||
|
|
||||||
|
// CurrencyAPIResponse structure for exchange rate API
|
||||||
|
type CurrencyAPIResponse struct {
|
||||||
|
Rates map[string]float64 `json:"rates"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateExchangeRates fetches and caches currency rates
|
||||||
|
func UpdateExchangeRates() error {
|
||||||
|
exchangeCacheMutex.Lock()
|
||||||
|
defer exchangeCacheMutex.Unlock()
|
||||||
|
|
||||||
|
// Use a reliable free API with good rate limits
|
||||||
|
resp, err := http.Get("https://open.er-api.com/v6/latest/USD")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var apiResponse struct {
|
||||||
|
Result string `json:"result"`
|
||||||
|
Rates map[string]float64 `json:"rates"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(body, &apiResponse); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if apiResponse.Result != "success" {
|
||||||
|
return fmt.Errorf("API error: %s", apiResponse.Result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache the rates
|
||||||
|
exchangeRates = apiResponse.Rates
|
||||||
|
lastUpdated = time.Now()
|
||||||
|
|
||||||
|
// Update list of all currencies
|
||||||
|
allCurrencies = make([]string, 0, len(exchangeRates))
|
||||||
|
for currency := range exchangeRates {
|
||||||
|
allCurrencies = append(allCurrencies, currency)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Updated currency rates: %d currencies cached", len(allCurrencies))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrecacheAllCurrencyPairs pre-caches conversion rates for all currency pairs
|
||||||
|
func PrecacheAllCurrencyPairs() {
|
||||||
|
exchangeCacheMutex.RLock()
|
||||||
|
defer exchangeCacheMutex.RUnlock()
|
||||||
|
|
||||||
|
if len(exchangeRates) == 0 {
|
||||||
|
log.Println("Skipping precache - no rates available")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Starting precache of all currency pairs (%d currencies)", len(allCurrencies))
|
||||||
|
|
||||||
|
// We'll pre-cache the most common pairs directly
|
||||||
|
commonCurrencies := []string{"USD", "EUR", "GBP", "JPY", "CAD", "AUD", "CHF", "CNY", "INR"}
|
||||||
|
|
||||||
|
for _, from := range commonCurrencies {
|
||||||
|
for _, to := range allCurrencies {
|
||||||
|
if from == to {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
GetExchangeRate(from, to)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Println("Common currency pairs precached")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetExchangeRate gets the current exchange rate with caching
|
||||||
|
func GetExchangeRate(from, to string) (float64, bool) {
|
||||||
|
// Auto-update cache if expired
|
||||||
|
if time.Since(lastUpdated) > cacheExpiration {
|
||||||
|
UpdateExchangeRates() // Ignore errors, use stale data if needed
|
||||||
|
}
|
||||||
|
|
||||||
|
exchangeCacheMutex.RLock()
|
||||||
|
defer exchangeCacheMutex.RUnlock()
|
||||||
|
|
||||||
|
// Handle same currency
|
||||||
|
if from == to {
|
||||||
|
return 1, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert via USD if direct rate not available
|
||||||
|
fromRate, fromExists := exchangeRates[from]
|
||||||
|
toRate, toExists := exchangeRates[to]
|
||||||
|
|
||||||
|
if !fromExists || !toExists {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate cross rate: (1 USD / fromRate) * toRate
|
||||||
|
return toRate / fromRate, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseCurrencyConversion detects and processes currency conversion queries
|
||||||
|
func ParseCurrencyConversion(query string) (float64, string, string, bool) {
|
||||||
|
// Match patterns like: "100 USD to EUR", "50 eur in gbp", "¥1000 to USD"
|
||||||
|
re := regexp.MustCompile(`(?i)([\d,]+(?:\.\d+)?)\s*([$€£¥₩₹₽A-Za-z]{1,6})\s+(?:to|in|➞|→)\s+([$€£¥₩₹₽A-Za-z]{1,6})`)
|
||||||
|
matches := re.FindStringSubmatch(query)
|
||||||
|
if len(matches) < 4 {
|
||||||
|
return 0, "", "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean and parse amount
|
||||||
|
amountStr := strings.ReplaceAll(matches[1], ",", "")
|
||||||
|
amount, err := strconv.ParseFloat(amountStr, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, "", "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize currency symbols
|
||||||
|
currencyMap := map[string]string{
|
||||||
|
"$": "USD", "€": "EUR", "£": "GBP", "¥": "JPY", "₩": "KRW", "₹": "INR", "₽": "RUB",
|
||||||
|
"usd": "USD", "eur": "EUR", "gbp": "GBP", "jpy": "JPY", "krw": "KRW", "inr": "INR", "rub": "RUB",
|
||||||
|
"dollar": "USD", "euro": "EUR", "pound": "GBP", "yen": "JPY", "won": "KRW", "rupee": "INR", "ruble": "RUB",
|
||||||
|
}
|
||||||
|
|
||||||
|
fromCurr := strings.ToUpper(matches[2])
|
||||||
|
if mapped, ok := currencyMap[fromCurr]; ok {
|
||||||
|
fromCurr = mapped
|
||||||
|
} else if len(fromCurr) > 3 {
|
||||||
|
// Try to match longer names
|
||||||
|
for k, v := range currencyMap {
|
||||||
|
if strings.EqualFold(k, fromCurr) {
|
||||||
|
fromCurr = v
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toCurr := strings.ToUpper(matches[3])
|
||||||
|
if mapped, ok := currencyMap[toCurr]; ok {
|
||||||
|
toCurr = mapped
|
||||||
|
} else if len(toCurr) > 3 {
|
||||||
|
// Try to match longer names
|
||||||
|
for k, v := range currencyMap {
|
||||||
|
if strings.EqualFold(k, toCurr) {
|
||||||
|
toCurr = v
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return amount, fromCurr, toCurr, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertCurrency handles the actual conversion
|
||||||
|
func ConvertCurrency(amount float64, from, to string) (float64, bool) {
|
||||||
|
if from == to {
|
||||||
|
return amount, true
|
||||||
|
}
|
||||||
|
|
||||||
|
rate, ok := GetExchangeRate(from, to)
|
||||||
|
if !ok {
|
||||||
|
// Try to find similar currencies
|
||||||
|
from = strings.ToUpper(from)
|
||||||
|
to = strings.ToUpper(to)
|
||||||
|
|
||||||
|
// Check if we have the currency in our list
|
||||||
|
exchangeCacheMutex.RLock()
|
||||||
|
defer exchangeCacheMutex.RUnlock()
|
||||||
|
|
||||||
|
_, fromExists := exchangeRates[from]
|
||||||
|
_, toExists := exchangeRates[to]
|
||||||
|
|
||||||
|
if !fromExists || !toExists {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shouldn't happen due to the check above, but just in case
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return amount * rate, true
|
||||||
|
}
|
83
ia-main.go
Normal file
83
ia-main.go
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type InstantAnswer struct {
|
||||||
|
Type string // "calc", "unit_convert", "wiki", ...
|
||||||
|
Title string
|
||||||
|
Content interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectInstantAnswer(query string) *InstantAnswer {
|
||||||
|
// Try currency conversion first (more specific)
|
||||||
|
if amount, from, to, ok := ParseCurrencyConversion(query); ok {
|
||||||
|
if result, ok := ConvertCurrency(amount, from, to); ok {
|
||||||
|
return &InstantAnswer{
|
||||||
|
Type: "currency",
|
||||||
|
Title: "Currency Conversion",
|
||||||
|
Content: map[string]interface{}{
|
||||||
|
"from": from,
|
||||||
|
"to": to,
|
||||||
|
"amount": amount,
|
||||||
|
"result": result,
|
||||||
|
"display": fmt.Sprintf("%.2f %s = %.2f %s", amount, from, result, to),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try math expression
|
||||||
|
if result, ok := parseMathExpression(query); ok {
|
||||||
|
return &InstantAnswer{
|
||||||
|
Type: "calc",
|
||||||
|
Title: "Calculation Result",
|
||||||
|
Content: result,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try Wikipedia search
|
||||||
|
if title, text, link, ok := getWikipediaSummary(query); ok {
|
||||||
|
return &InstantAnswer{
|
||||||
|
Type: "wiki",
|
||||||
|
Title: title,
|
||||||
|
Content: map[string]string{
|
||||||
|
"text": text,
|
||||||
|
"link": link,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func initExchangeRates() {
|
||||||
|
// Initial synchronous load
|
||||||
|
if err := UpdateExchangeRates(); err != nil {
|
||||||
|
printErr("Initial exchange rate update failed: %v", err)
|
||||||
|
} else {
|
||||||
|
PrecacheAllCurrencyPairs()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre-cache common wiki terms in background
|
||||||
|
go func() {
|
||||||
|
commonTerms := []string{"United States", "Europe", "Technology", "Science", "Mathematics"}
|
||||||
|
for _, term := range commonTerms {
|
||||||
|
getWikipediaSummary(term)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Periodically update cache
|
||||||
|
ticker := time.NewTicker(30 * time.Minute)
|
||||||
|
go func() {
|
||||||
|
for range ticker.C {
|
||||||
|
if err := UpdateExchangeRates(); err != nil {
|
||||||
|
printWarn("Periodic exchange rate update failed: %v", err)
|
||||||
|
} else {
|
||||||
|
PrecacheAllCurrencyPairs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
74
ia-wiki.go
Normal file
74
ia-wiki.go
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Wikipedia API response structure
|
||||||
|
type WikipediaResponse struct {
|
||||||
|
Query struct {
|
||||||
|
Pages map[string]struct {
|
||||||
|
PageID int `json:"pageid"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Extract string `json:"extract"`
|
||||||
|
} `json:"pages"`
|
||||||
|
} `json:"query"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get Wikipedia summary
|
||||||
|
func getWikipediaSummary(query string) (title, text, link string, ok bool) {
|
||||||
|
// Clean and prepare query
|
||||||
|
query = strings.TrimSpace(query)
|
||||||
|
if query == "" {
|
||||||
|
return "", "", "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// API request
|
||||||
|
apiURL := fmt.Sprintf(
|
||||||
|
"https://en.wikipedia.org/w/api.php?action=query&format=json&prop=extracts&exintro&explaintext&redirects=1&titles=%s",
|
||||||
|
url.QueryEscape(query),
|
||||||
|
)
|
||||||
|
|
||||||
|
resp, err := http.Get(apiURL)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", "", false
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse JSON response
|
||||||
|
var result WikipediaResponse
|
||||||
|
if err := json.Unmarshal(body, &result); err != nil {
|
||||||
|
return "", "", "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract first valid page
|
||||||
|
for _, page := range result.Query.Pages {
|
||||||
|
if page.PageID == 0 || page.Extract == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format text
|
||||||
|
text = page.Extract
|
||||||
|
if len(text) > 500 {
|
||||||
|
text = text[:500] + "..."
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create link
|
||||||
|
titleForURL := strings.ReplaceAll(page.Title, " ", "_")
|
||||||
|
link = fmt.Sprintf("https://en.wikipedia.org/wiki/%s", url.PathEscape(titleForURL))
|
||||||
|
|
||||||
|
return page.Title, text, link, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", "", "", false
|
||||||
|
}
|
|
@ -72,6 +72,7 @@ func main() {
|
||||||
initFileEngines()
|
initFileEngines()
|
||||||
initPipedInstances()
|
initPipedInstances()
|
||||||
initMusicEngines()
|
initMusicEngines()
|
||||||
|
initExchangeRates()
|
||||||
}
|
}
|
||||||
|
|
||||||
InitializeLanguage("en") // Initialize language before generating OpenSearch
|
InitializeLanguage("en") // Initialize language before generating OpenSearch
|
||||||
|
|
1
init.go
1
init.go
|
@ -71,6 +71,7 @@ func main() {
|
||||||
initFileEngines()
|
initFileEngines()
|
||||||
initPipedInstances()
|
initPipedInstances()
|
||||||
initMusicEngines()
|
initMusicEngines()
|
||||||
|
initExchangeRates()
|
||||||
}
|
}
|
||||||
|
|
||||||
InitializeLanguage("en") // Initialize language before generating OpenSearch
|
InitializeLanguage("en") // Initialize language before generating OpenSearch
|
||||||
|
|
2
main.go
2
main.go
|
@ -175,7 +175,7 @@ func handleSearch(w http.ResponseWriter, r *http.Request) {
|
||||||
case "text":
|
case "text":
|
||||||
fallthrough
|
fallthrough
|
||||||
default:
|
default:
|
||||||
HandleTextSearch(w, settings, query, page)
|
HandleTextSearchWithInstantAnswer(w, settings, query, page)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
139
static/css/style-instantanswer.css
Normal file
139
static/css/style-instantanswer.css
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
.instant-container {
|
||||||
|
position: absolute;
|
||||||
|
top: 140px;
|
||||||
|
right: 175px;
|
||||||
|
width: 500px;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instant-box {
|
||||||
|
border: 1px solid var(--snip-border);
|
||||||
|
background-color: var(--snip-background);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.instant-box h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: var(--highlight);
|
||||||
|
font-size: 20px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
padding-bottom: 10px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instant-result {
|
||||||
|
margin: 15px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calc-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calc-input {
|
||||||
|
padding: 10px;
|
||||||
|
width: 95%;
|
||||||
|
font-size: 18px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: var(--search-bg);
|
||||||
|
color: var(--fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calc-buttons {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calc-buttons button {
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 18px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: var(--button);
|
||||||
|
color: var(--fg);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calc-buttons button:hover {
|
||||||
|
background-color: var(--search-select);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calc-result {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 20px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: var(--snip-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calc-history {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--fg);
|
||||||
|
background-color: var(--search-bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
height: 120px;
|
||||||
|
overflow-y: auto;
|
||||||
|
white-space: pre-line;
|
||||||
|
overflow-anchor: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calc-buttons {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calc-buttons button {
|
||||||
|
padding: 20px;
|
||||||
|
font-size: 20px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: var(--search-bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: var(--fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calc-buttons .equals {
|
||||||
|
background-color: #2ecc71;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calc-buttons .equals:hover {
|
||||||
|
background-color: #27ae60;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media only screen and (max-width: 1220px) {
|
||||||
|
.instant-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
margin-left: 175px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 880px) {
|
||||||
|
.instant-container {
|
||||||
|
width: 90%;
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
}
|
|
@ -123,93 +123,6 @@ html {
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.calc {
|
|
||||||
height: fit-content;
|
|
||||||
width: fit-content;
|
|
||||||
position: relative;
|
|
||||||
left: 175px;
|
|
||||||
border: 1px solid var(--snip-border);
|
|
||||||
background-color: var(--snip-background);
|
|
||||||
border-radius: 8px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calc-btn-style {
|
|
||||||
background-color: var(--html-bg) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calc-input {
|
|
||||||
width: 90%;
|
|
||||||
height: 10%;
|
|
||||||
background-color: var(--search-bg);
|
|
||||||
border: 1px solid var(--snip-border);
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 15px;
|
|
||||||
margin-top: 8px;
|
|
||||||
text-align: right;
|
|
||||||
max-width: 48em;
|
|
||||||
line-height: 1.58;
|
|
||||||
font-size: 22px;
|
|
||||||
color: var(--fg);
|
|
||||||
letter-spacing: normal;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calc-btn {
|
|
||||||
max-width: 48em;
|
|
||||||
line-height: 50px;
|
|
||||||
font-size: 22px;
|
|
||||||
color: var(--fg);
|
|
||||||
letter-spacing: normal;
|
|
||||||
border-radius: 8px;
|
|
||||||
background-color: var(--search-bg);
|
|
||||||
border: 1px solid var(--snip-border);
|
|
||||||
height: 50px;
|
|
||||||
margin: auto;
|
|
||||||
margin: 4px;
|
|
||||||
width: 80px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calc-btn-2 {
|
|
||||||
max-width: 48em;
|
|
||||||
line-height: 50px;
|
|
||||||
font-size: 22px;
|
|
||||||
color: var(--fff);
|
|
||||||
letter-spacing: normal;
|
|
||||||
border-radius: 8px;
|
|
||||||
background-color: var(--font-fg);
|
|
||||||
height: 50px;
|
|
||||||
margin: auto;
|
|
||||||
margin: 4px;
|
|
||||||
width: 80px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calc-btns {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(4, 90px);
|
|
||||||
width: 100%;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calc-pos-absolute {
|
|
||||||
position: absolute;
|
|
||||||
margin-top: 60px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prev_calculation {
|
|
||||||
opacity: 0.5;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-code {
|
.emoji-code {
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
}
|
}
|
||||||
|
@ -978,6 +891,7 @@ p {
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
margin-bottom: 35px;
|
margin-bottom: 35px;
|
||||||
|
z-index: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.result_sublink {
|
.result_sublink {
|
||||||
|
@ -1518,6 +1432,10 @@ button {
|
||||||
|
|
||||||
@media only screen and (max-width: 1220px) {
|
@media only screen and (max-width: 1220px) {
|
||||||
|
|
||||||
|
.results {
|
||||||
|
padding-right: 340px;
|
||||||
|
}
|
||||||
|
|
||||||
.snip {
|
.snip {
|
||||||
position: relative;
|
position: relative;
|
||||||
float: none;
|
float: none;
|
||||||
|
@ -1541,10 +1459,6 @@ button {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.calc {
|
|
||||||
left: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.results h3,
|
.results h3,
|
||||||
.result_sublink h3 {
|
.result_sublink h3 {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
|
47
static/js/calculator.js
Normal file
47
static/js/calculator.js
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
const calcContainer = document.getElementById('dynamic-calc');
|
||||||
|
const staticCalc = document.getElementById('static-calc');
|
||||||
|
|
||||||
|
if (calcContainer) {
|
||||||
|
calcContainer.style.display = 'block';
|
||||||
|
if (staticCalc) staticCalc.style.display = 'none';
|
||||||
|
|
||||||
|
const input = document.getElementById('calc-input');
|
||||||
|
const result = document.getElementById('calc-result');
|
||||||
|
const history = document.getElementById('calc-history');
|
||||||
|
const buttons = document.querySelectorAll('.calc-buttons button');
|
||||||
|
|
||||||
|
let currentInput = '';
|
||||||
|
let historyLog = [];
|
||||||
|
|
||||||
|
const updateUI = () => {
|
||||||
|
input.value = currentInput;
|
||||||
|
history.innerHTML = historyLog.map(entry => `<div>${entry}</div>`).join('');
|
||||||
|
};
|
||||||
|
|
||||||
|
buttons.forEach(button => {
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
const val = button.getAttribute('data-value');
|
||||||
|
|
||||||
|
if (val === 'C') {
|
||||||
|
currentInput = '';
|
||||||
|
} else if (val === '=') {
|
||||||
|
try {
|
||||||
|
const res = eval(currentInput);
|
||||||
|
historyLog.push(`${currentInput} = ${res}`);
|
||||||
|
if (historyLog.length > 30) historyLog.shift(); // Remove oldest
|
||||||
|
currentInput = res.toString();
|
||||||
|
} catch (e) {
|
||||||
|
currentInput = 'Error';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
currentInput += val;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUI();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
updateUI();
|
||||||
|
}
|
||||||
|
});
|
|
@ -10,6 +10,7 @@
|
||||||
<title>{{ .Query }} - {{ translate "site_name" }}</title>
|
<title>{{ .Query }} - {{ translate "site_name" }}</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="stylesheet" href="/static/css/style-instantanswer.css">
|
||||||
<link rel="stylesheet" href="/static/css/style-fonts.css">
|
<link rel="stylesheet" href="/static/css/style-fonts.css">
|
||||||
<link rel="stylesheet" href="/static/css/style-loadingcircle.css">
|
<link rel="stylesheet" href="/static/css/style-loadingcircle.css">
|
||||||
<link rel="stylesheet" href="/static/css/style-menu.css">
|
<link rel="stylesheet" href="/static/css/style-menu.css">
|
||||||
|
@ -163,6 +164,58 @@
|
||||||
</form>
|
</form>
|
||||||
<p class="fetched fetched_dif_files fetched_tor">{{ translate "fetched_in" .Fetched }}</p>
|
<p class="fetched fetched_dif_files fetched_tor">{{ translate "fetched_in" .Fetched }}</p>
|
||||||
|
|
||||||
|
<!-- Instant Answers Container -->
|
||||||
|
{{if .Instant}}
|
||||||
|
<div class="instant-container">
|
||||||
|
<div class="instant-box">
|
||||||
|
<h3>{{.Instant.Title}}</h3>
|
||||||
|
{{if eq .Instant.Type "currency"}}
|
||||||
|
<div class="instant-result">
|
||||||
|
{{index .Instant.Content "display"}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{if eq .Instant.Type "calc"}}
|
||||||
|
<div class="instant-result">
|
||||||
|
<!-- Static result for non-JS users -->
|
||||||
|
<div id="static-calc">{{.Instant.Content}}</div>
|
||||||
|
|
||||||
|
<!-- Calculator UI for JS users -->
|
||||||
|
<div id="dynamic-calc" class="calc-container" style="display:none;">
|
||||||
|
<div class="calc-history" id="calc-history"></div>
|
||||||
|
<input type="text" class="calc-input" id="calc-input" value="{{.Query}}" readonly>
|
||||||
|
<div class="calc-buttons">
|
||||||
|
<button data-value="C">⌫</button>
|
||||||
|
<button data-value="(">(</button>
|
||||||
|
<button data-value=")">)</button>
|
||||||
|
<button data-value="%">mod</button>
|
||||||
|
<button data-value="7">7</button>
|
||||||
|
<button data-value="8">8</button>
|
||||||
|
<button data-value="9">9</button>
|
||||||
|
<button data-value="/">÷</button>
|
||||||
|
<button data-value="4">4</button>
|
||||||
|
<button data-value="5">5</button>
|
||||||
|
<button data-value="6">6</button>
|
||||||
|
<button data-value="*">×</button>
|
||||||
|
<button data-value="1">1</button>
|
||||||
|
<button data-value="2">2</button>
|
||||||
|
<button data-value="3">3</button>
|
||||||
|
<button data-value="-">-</button>
|
||||||
|
<button data-value="0">0</button>
|
||||||
|
<button data-value=".">,</button>
|
||||||
|
<button data-value="=" class="equals">=</button>
|
||||||
|
<button data-value="+">+</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{if eq .Instant.Type "wiki"}}
|
||||||
|
<div class="instant-result">{{index .Instant.Content "text"}}</div>
|
||||||
|
<div><a href="{{index .Instant.Content "link"}}" target="_blank">Read more on Wikipedia →</a></div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
<div class="results" id="results">
|
<div class="results" id="results">
|
||||||
{{ if .Results }}
|
{{ if .Results }}
|
||||||
{{ range .Results }}
|
{{ range .Results }}
|
||||||
|
@ -196,10 +249,12 @@
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="message-bottom-right" class="message-bottom-right">
|
<div id="message-bottom-right" class="message-bottom-right">
|
||||||
<span id="loading-text">{{ translate "searching_for_new_results" }}</span><span class="dot">.</span><span
|
<span id="loading-text">{{ translate "searching_for_new_results" }}</span><span class="dot">.</span><span
|
||||||
class="dot">.</span><span class="dot">.</span>
|
class="dot">.</span><span class="dot">.</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="prev-next prev-img" id="prev-next">
|
<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 }}">
|
||||||
|
@ -218,6 +273,7 @@
|
||||||
data-hard-cache-enabled="{{ .HardCacheEnabled }}"></div>
|
data-hard-cache-enabled="{{ .HardCacheEnabled }}"></div>
|
||||||
<script defer src="/static/js/dynamicscrollingtext.js"></script>
|
<script defer src="/static/js/dynamicscrollingtext.js"></script>
|
||||||
<script defer src="/static/js/autocomplete.js"></script>
|
<script defer src="/static/js/autocomplete.js"></script>
|
||||||
|
<script defer src="/static/js/calculator.js"></script>
|
||||||
<script defer src="/static/js/minimenu.js"></script>
|
<script defer src="/static/js/minimenu.js"></script>
|
||||||
<script>
|
<script>
|
||||||
document.querySelectorAll('.js-enabled').forEach(el => el.classList.remove('js-enabled'));
|
document.querySelectorAll('.js-enabled').forEach(el => el.classList.remove('js-enabled'));
|
||||||
|
|
|
@ -36,6 +36,90 @@ func initTextEngines() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func HandleTextSearchWithInstantAnswer(w http.ResponseWriter, settings UserSettings, query string, page int) {
|
||||||
|
startTime := time.Now()
|
||||||
|
|
||||||
|
var instant *InstantAnswer
|
||||||
|
done := make(chan struct{})
|
||||||
|
|
||||||
|
// Spawn instant detection in background
|
||||||
|
go func() {
|
||||||
|
instant = detectInstantAnswer(query)
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Call original handler and capture its result template data
|
||||||
|
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
|
||||||
|
|
||||||
|
go prefetchPage(query, settings.SafeSearch, settings.SearchLanguage, page+1)
|
||||||
|
if hasPrevPage {
|
||||||
|
go prefetchPage(query, settings.SafeSearch, settings.SearchLanguage, page-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
elapsedTime := time.Since(startTime)
|
||||||
|
|
||||||
|
type DecoratedResult struct {
|
||||||
|
TextSearchResult
|
||||||
|
PrettyLink LinkParts
|
||||||
|
FaviconID string
|
||||||
|
}
|
||||||
|
var decoratedResults []DecoratedResult
|
||||||
|
for _, r := range combinedResults {
|
||||||
|
if r.URL == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
prettyLink := FormatLinkHTML(r.URL)
|
||||||
|
faviconID := faviconIDFromURL(prettyLink.RootURL)
|
||||||
|
|
||||||
|
decoratedResults = append(decoratedResults, DecoratedResult{
|
||||||
|
TextSearchResult: r,
|
||||||
|
PrettyLink: prettyLink,
|
||||||
|
FaviconID: faviconID,
|
||||||
|
})
|
||||||
|
|
||||||
|
go ensureFaviconIsCached(faviconID, prettyLink.RootURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait max 300ms for instant answer detection
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
case <-time.After(300 * time.Millisecond):
|
||||||
|
printInfo("InstantAnswer detection timed out")
|
||||||
|
}
|
||||||
|
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"Results": decoratedResults,
|
||||||
|
"Query": query,
|
||||||
|
"Fetched": FormatElapsedTime(elapsedTime),
|
||||||
|
"Page": page,
|
||||||
|
"HasPrevPage": hasPrevPage,
|
||||||
|
"HasNextPage": len(combinedResults) >= 50,
|
||||||
|
"NoResults": len(combinedResults) == 0,
|
||||||
|
"LanguageOptions": languageOptions,
|
||||||
|
"CurrentLang": settings.SearchLanguage,
|
||||||
|
"Theme": settings.Theme,
|
||||||
|
"Safe": settings.SafeSearch,
|
||||||
|
"IsThemeDark": settings.IsThemeDark,
|
||||||
|
"Trans": Translate,
|
||||||
|
"HardCacheEnabled": config.DriveCacheEnabled,
|
||||||
|
}
|
||||||
|
|
||||||
|
if instant != nil {
|
||||||
|
data["Instant"] = instant
|
||||||
|
}
|
||||||
|
|
||||||
|
renderTemplate(w, "text.html", data)
|
||||||
|
}
|
||||||
|
|
||||||
func HandleTextSearch(w http.ResponseWriter, settings UserSettings, query string, page int) {
|
func HandleTextSearch(w http.ResponseWriter, settings UserSettings, query string, page int) {
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
|
|
||||||
|
|
84
text.go
84
text.go
|
@ -36,6 +36,90 @@ func initTextEngines() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func HandleTextSearchWithInstantAnswer(w http.ResponseWriter, settings UserSettings, query string, page int) {
|
||||||
|
startTime := time.Now()
|
||||||
|
|
||||||
|
var instant *InstantAnswer
|
||||||
|
done := make(chan struct{})
|
||||||
|
|
||||||
|
// Spawn instant detection in background
|
||||||
|
go func() {
|
||||||
|
instant = detectInstantAnswer(query)
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Call original handler and capture its result template data
|
||||||
|
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
|
||||||
|
|
||||||
|
go prefetchPage(query, settings.SafeSearch, settings.SearchLanguage, page+1)
|
||||||
|
if hasPrevPage {
|
||||||
|
go prefetchPage(query, settings.SafeSearch, settings.SearchLanguage, page-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
elapsedTime := time.Since(startTime)
|
||||||
|
|
||||||
|
type DecoratedResult struct {
|
||||||
|
TextSearchResult
|
||||||
|
PrettyLink LinkParts
|
||||||
|
FaviconID string
|
||||||
|
}
|
||||||
|
var decoratedResults []DecoratedResult
|
||||||
|
for _, r := range combinedResults {
|
||||||
|
if r.URL == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
prettyLink := FormatLinkHTML(r.URL)
|
||||||
|
faviconID := faviconIDFromURL(prettyLink.RootURL)
|
||||||
|
|
||||||
|
decoratedResults = append(decoratedResults, DecoratedResult{
|
||||||
|
TextSearchResult: r,
|
||||||
|
PrettyLink: prettyLink,
|
||||||
|
FaviconID: faviconID,
|
||||||
|
})
|
||||||
|
|
||||||
|
go ensureFaviconIsCached(faviconID, prettyLink.RootURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait max 300ms for instant answer detection
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
case <-time.After(300 * time.Millisecond):
|
||||||
|
printInfo("InstantAnswer detection timed out")
|
||||||
|
}
|
||||||
|
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"Results": decoratedResults,
|
||||||
|
"Query": query,
|
||||||
|
"Fetched": FormatElapsedTime(elapsedTime),
|
||||||
|
"Page": page,
|
||||||
|
"HasPrevPage": hasPrevPage,
|
||||||
|
"HasNextPage": len(combinedResults) >= 50,
|
||||||
|
"NoResults": len(combinedResults) == 0,
|
||||||
|
"LanguageOptions": languageOptions,
|
||||||
|
"CurrentLang": settings.SearchLanguage,
|
||||||
|
"Theme": settings.Theme,
|
||||||
|
"Safe": settings.SafeSearch,
|
||||||
|
"IsThemeDark": settings.IsThemeDark,
|
||||||
|
"Trans": Translate,
|
||||||
|
"HardCacheEnabled": config.DriveCacheEnabled,
|
||||||
|
}
|
||||||
|
|
||||||
|
if instant != nil {
|
||||||
|
data["Instant"] = instant
|
||||||
|
}
|
||||||
|
|
||||||
|
renderTemplate(w, "text.html", data)
|
||||||
|
}
|
||||||
|
|
||||||
func HandleTextSearch(w http.ResponseWriter, settings UserSettings, query string, page int) {
|
func HandleTextSearch(w http.ResponseWriter, settings UserSettings, query string, page int) {
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue