This commit is contained in:
parent
e2f1707723
commit
0559fd2bba
5 changed files with 623 additions and 2 deletions
39
common.go
39
common.go
|
@ -29,9 +29,48 @@ var (
|
||||||
}
|
}
|
||||||
return string(jsonBytes), nil
|
return string(jsonBytes), nil
|
||||||
},
|
},
|
||||||
|
"formatShortDate": func(date string) string {
|
||||||
|
t, _ := time.Parse("2006-01-02", date)
|
||||||
|
// return t.Format("Mon") // e.g. "Sat"
|
||||||
|
return t.Format("2.1.") // e.g. "29.6."
|
||||||
|
},
|
||||||
|
"weatherIcon": func(cur interface{}) string {
|
||||||
|
switch c := cur.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
if cond, ok := c["Condition"].(string); ok {
|
||||||
|
return iconForCond(cond)
|
||||||
|
}
|
||||||
|
case WeatherCurrent:
|
||||||
|
return iconForCond(c.Condition)
|
||||||
|
case *WeatherCurrent:
|
||||||
|
return iconForCond(c.Condition)
|
||||||
|
}
|
||||||
|
return "🌈"
|
||||||
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func iconForCond(cond string) string {
|
||||||
|
switch cond {
|
||||||
|
case "Clear":
|
||||||
|
return "☀️"
|
||||||
|
case "Partly cloudy":
|
||||||
|
return "⛅"
|
||||||
|
case "Cloudy":
|
||||||
|
return "☁️"
|
||||||
|
case "Rain":
|
||||||
|
return "🌧️"
|
||||||
|
case "Snow":
|
||||||
|
return "❄️"
|
||||||
|
case "Thunderstorm":
|
||||||
|
return "⛈️"
|
||||||
|
case "Fog":
|
||||||
|
return "🌫️"
|
||||||
|
default:
|
||||||
|
return "🌈"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type SearchEngine struct {
|
type SearchEngine struct {
|
||||||
Name string
|
Name string
|
||||||
Func func(string, string, string, int) ([]SearchResult, time.Duration, error)
|
Func func(string, string, string, int) ([]SearchResult, time.Duration, error)
|
||||||
|
|
17
ia-main.go
17
ia-main.go
|
@ -38,6 +38,23 @@ func detectInstantAnswer(query string) *InstantAnswer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try weather instant answer
|
||||||
|
if city, forecast, ok := getWeatherForQuery(query); ok {
|
||||||
|
return &InstantAnswer{
|
||||||
|
Type: "weather",
|
||||||
|
Title: fmt.Sprintf("Weather in %s", city.Name),
|
||||||
|
Content: map[string]interface{}{
|
||||||
|
"city": city.Name,
|
||||||
|
"country": city.Country,
|
||||||
|
"lat": city.Lat,
|
||||||
|
"lon": city.Lon,
|
||||||
|
"current": forecast.Current,
|
||||||
|
"forecast": forecast.Forecast,
|
||||||
|
"display": fmt.Sprintf("%.1f°C, %s", forecast.Current.Temperature, forecast.Current.Condition),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Try Wikipedia search
|
// Try Wikipedia search
|
||||||
if title, text, link, ok := getWikipediaSummary(query); ok {
|
if title, text, link, ok := getWikipediaSummary(query); ok {
|
||||||
return &InstantAnswer{
|
return &InstantAnswer{
|
||||||
|
|
367
ia-weather.go
Normal file
367
ia-weather.go
Normal file
|
@ -0,0 +1,367 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
|
"golang.org/x/text/unicode/norm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WeatherCity struct {
|
||||||
|
Name string
|
||||||
|
Country string
|
||||||
|
Lat float64
|
||||||
|
Lon float64
|
||||||
|
}
|
||||||
|
type WeatherCurrent struct {
|
||||||
|
Temperature float64
|
||||||
|
Wind float64
|
||||||
|
Humidity int
|
||||||
|
Condition string
|
||||||
|
}
|
||||||
|
type WeatherDay struct {
|
||||||
|
Date string
|
||||||
|
MinTemp float64
|
||||||
|
MaxTemp float64
|
||||||
|
Condition string
|
||||||
|
}
|
||||||
|
type WeatherForecast struct {
|
||||||
|
Current WeatherCurrent
|
||||||
|
Forecast []WeatherDay
|
||||||
|
}
|
||||||
|
|
||||||
|
func getWeatherForQuery(query string) (city WeatherCity, forecast WeatherForecast, ok bool) {
|
||||||
|
// Expanded multi-language weather keywords (40+ languages/dialects)
|
||||||
|
weatherWords := []string{
|
||||||
|
// English
|
||||||
|
"weather", "forecast", "temperature", "conditions", "meteorology", "outlook",
|
||||||
|
// Czech/Slovak
|
||||||
|
"počasí", "předpověď", "teplota", "vlhkost", "srážky", "vítr", "meteo",
|
||||||
|
// German
|
||||||
|
"wetter", "vorhersage", "temperatur", "wettervorhersage", "wetterbericht",
|
||||||
|
// French
|
||||||
|
"météo", "prévisions", "température", "conditions météo", "prévision météo",
|
||||||
|
// Spanish
|
||||||
|
"tiempo", "clima", "pronóstico", "temperatura", "meteorología", "previsión",
|
||||||
|
// Italian
|
||||||
|
"tempo", "meteo", "previsioni", "temperatura", "condizioni atmosferiche",
|
||||||
|
// Portuguese
|
||||||
|
"tempo", "clima", "previsão", "temperatura", "meteorologia",
|
||||||
|
// Polish
|
||||||
|
"pogoda", "prognoza", "temperatura", "warunki atmosferyczne",
|
||||||
|
// Russian
|
||||||
|
"погода", "прогноз", "температура", "метео", "метеопрогноз",
|
||||||
|
// Ukrainian
|
||||||
|
"погода", "прогноз", "температура", "метео",
|
||||||
|
// Dutch
|
||||||
|
"weer", "voorspelling", "temperatuur", "weersverwachting",
|
||||||
|
// Scandinavian
|
||||||
|
"väder", "prognos", "temperatur", // Swedish
|
||||||
|
"vær", "prognose", "temperatur", // Norwegian/Danish
|
||||||
|
"veður", "spá", "hitastig", // Icelandic
|
||||||
|
// East Asian
|
||||||
|
"天気", "予報", "気温", // Japanese (tenki, yohō, kion)
|
||||||
|
"날씨", "예보", "기온", // Korean (nalssi, yebo, gion)
|
||||||
|
"天气", "预报", "气温", // Chinese (tiānqì, yùbào, qìwēn)
|
||||||
|
// South Asian
|
||||||
|
"मौसम", "पूर्वानुमान", "तापमान", // Hindi (mausam, purvanumaan, taapmaan)
|
||||||
|
"আবহাওয়া", "পূর্বাভাস", "তাপমাত্রা", // Bengali (ābhawāẏā, pūrbābhāsa, tāpamātrā)
|
||||||
|
// Middle Eastern
|
||||||
|
"طقس", "توقعات", "درجة الحرارة", // Arabic (ṭaqs, tawaqquʿāt, darajat al-ḥarāra)
|
||||||
|
"آب و ہوا", "پیش گوئی", "درجہ حرارت", // Urdu (āb-o-hawā, peshgoī, daraja ḥarārat)
|
||||||
|
// Turkish
|
||||||
|
"hava", "tahmin", "sıcaklık", "hava durumu",
|
||||||
|
// Greek
|
||||||
|
"καιρός", "πρόβλεψη", "θερμοκρασία",
|
||||||
|
// Hebrew
|
||||||
|
"מזג אוויר", "תחזית", "טמפרטורה",
|
||||||
|
// Other European
|
||||||
|
"időkép", "előrejelzés", "hőmérséklet", // Hungarian
|
||||||
|
"vreme", "prognoză", "temperatură", // Romanian
|
||||||
|
"vrijeme", "prognoza", "temperatura", // Croatian/Serbian
|
||||||
|
// Global/Internet slang
|
||||||
|
"temp", "wx", "meteo", "wea", "forec",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced multi-language prepositions
|
||||||
|
prepositions := []string{
|
||||||
|
// English
|
||||||
|
"in", "at", "for", "around", "near",
|
||||||
|
// Czech/Slovak
|
||||||
|
"v", "ve", "na", "do", "u", "při", "blízko", "okolí",
|
||||||
|
// German
|
||||||
|
"in", "bei", "an", "für", "um", "nahe",
|
||||||
|
// Romance
|
||||||
|
"en", "a", "au", "aux", "dans", // French
|
||||||
|
"en", "a", "de", // Spanish
|
||||||
|
"a", "in", "da", // Italian
|
||||||
|
"em", "no", "na", // Portuguese
|
||||||
|
// Slavic
|
||||||
|
"w", "we", "na", "dla", "pod", // Polish
|
||||||
|
"в", "на", "у", "к", "под", // Russian/Ukrainian
|
||||||
|
// Nordic
|
||||||
|
"i", "på", "hos", // Swedish/Danish/Norwegian
|
||||||
|
// Others
|
||||||
|
"في", "عند", "قرب", // Arabic (fī, ʿind, qurb)
|
||||||
|
"में", "पर", "के पास", // Hindi (mẽ, par, ke pās)
|
||||||
|
"で", "に", "の近く", // Japanese (de, ni, no chikaku)
|
||||||
|
"에서", "에", "근처", // Korean (eseo, e, geuncheo)
|
||||||
|
"在", "于", "附近", // Chinese (zài, yú, fùjìn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always normalize query (lowercase + remove diacritics)
|
||||||
|
normalized := removeDiacritics(strings.ToLower(query))
|
||||||
|
hasWeather := false
|
||||||
|
for _, word := range weatherWords {
|
||||||
|
if strings.Contains(normalized, removeDiacritics(word)) {
|
||||||
|
hasWeather = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasWeather {
|
||||||
|
return city, forecast, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Improved location extraction with diacritic handling
|
||||||
|
loc := extractWeatherLocation(normalized, weatherWords, prepositions)
|
||||||
|
if loc == "" {
|
||||||
|
return city, forecast, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Geocode and get weather
|
||||||
|
return geocodeAndGetWeather(loc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractWeatherLocation(query string, weatherWords, prepositions []string) string {
|
||||||
|
// Create normalized versions for matching
|
||||||
|
normWeatherWords := make([]string, len(weatherWords))
|
||||||
|
for i, w := range weatherWords {
|
||||||
|
normWeatherWords[i] = removeDiacritics(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
normPrepositions := make([]string, len(prepositions))
|
||||||
|
for i, p := range prepositions {
|
||||||
|
normPrepositions[i] = removeDiacritics(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pattern 1: [weather_word] [preposition]? [location]
|
||||||
|
pattern1 := `(?:` + strings.Join(normWeatherWords, "|") + `)\s*(?:` + strings.Join(normPrepositions, "|") + `)?\s*(.+)`
|
||||||
|
re1 := regexp.MustCompile(pattern1)
|
||||||
|
if matches := re1.FindStringSubmatch(query); len(matches) > 1 {
|
||||||
|
loc := cleanLocation(matches[1], normPrepositions)
|
||||||
|
if loc != "" {
|
||||||
|
return loc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pattern 2: [location] [weather_word]
|
||||||
|
pattern2 := `(.+?)\s+(?:` + strings.Join(normWeatherWords, "|") + `)`
|
||||||
|
re2 := regexp.MustCompile(pattern2)
|
||||||
|
if matches := re2.FindStringSubmatch(query); len(matches) > 1 {
|
||||||
|
loc := cleanLocation(matches[1], normPrepositions)
|
||||||
|
if loc != "" {
|
||||||
|
return loc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pattern 3: Question format
|
||||||
|
questionPattern := `(?:how is|what is|what's|jak[ée]\s+je|wie ist|quel est|qu[eé]\s+tal|com'[èe])\s+(?:the )?(?:` +
|
||||||
|
strings.Join(normWeatherWords, "|") + `)\s*(?:` + strings.Join(normPrepositions, "|") + `)?\s*(.+)`
|
||||||
|
re3 := regexp.MustCompile(questionPattern)
|
||||||
|
if matches := re3.FindStringSubmatch(query); len(matches) > 1 {
|
||||||
|
loc := cleanLocation(matches[1], normPrepositions)
|
||||||
|
if loc != "" {
|
||||||
|
return loc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback with smarter exclusion
|
||||||
|
return extractByExclusion(query, normWeatherWords, normPrepositions)
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanLocation(loc string, prepositions []string) string {
|
||||||
|
// Create preposition set
|
||||||
|
prepSet := make(map[string]bool)
|
||||||
|
for _, p := range prepositions {
|
||||||
|
prepSet[p] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
words := strings.Fields(loc)
|
||||||
|
|
||||||
|
// Remove leading prepositions
|
||||||
|
for len(words) > 0 && prepSet[words[0]] {
|
||||||
|
words = words[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove trailing prepositions
|
||||||
|
for len(words) > 0 && prepSet[words[len(words)-1]] {
|
||||||
|
words = words[:len(words)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rejoin and clean
|
||||||
|
cleaned := strings.Join(words, " ")
|
||||||
|
return strings.Trim(cleaned, ",.?!:;()[]{}'\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove diacritics implementation
|
||||||
|
func removeDiacritics(s string) string {
|
||||||
|
var result []rune
|
||||||
|
for _, r := range norm.NFD.String(s) {
|
||||||
|
if unicode.Is(unicode.Mn, r) { // Mn: nonspacing marks
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result = append(result, r)
|
||||||
|
}
|
||||||
|
return string(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract location by removing weather-related words
|
||||||
|
func extractByExclusion(query string, weatherWords, prepositions []string) string {
|
||||||
|
// Create removal set
|
||||||
|
removeSet := make(map[string]bool)
|
||||||
|
for _, w := range weatherWords {
|
||||||
|
removeSet[w] = true
|
||||||
|
}
|
||||||
|
for _, p := range prepositions {
|
||||||
|
removeSet[p] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process query words
|
||||||
|
words := strings.Fields(query)
|
||||||
|
var locWords []string
|
||||||
|
for _, word := range words {
|
||||||
|
if !removeSet[word] {
|
||||||
|
locWords = append(locWords, word)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loc := strings.Join(locWords, " ")
|
||||||
|
return cleanLocation(loc, prepositions)
|
||||||
|
}
|
||||||
|
|
||||||
|
// // Improved location cleaning
|
||||||
|
// func cleanLocation(loc string) string {
|
||||||
|
// loc = strings.Trim(loc, ",.?!:;()[]{}'\"")
|
||||||
|
|
||||||
|
// // Remove trailing verbs
|
||||||
|
// verbs := []string{"is", "at", "for", "in", "v", "ve", "na", "do", "w", "en", "a"}
|
||||||
|
// for _, v := range verbs {
|
||||||
|
// loc = strings.TrimSuffix(loc, " "+v)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return loc
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Remove diacritics implementation
|
||||||
|
// func removeDiacritics(s string) string {
|
||||||
|
// var result []rune
|
||||||
|
// for _, r := range norm.NFD.String(s) {
|
||||||
|
// if unicode.Is(unicode.Mn, r) { // Mn: nonspacing marks
|
||||||
|
// continue
|
||||||
|
// }
|
||||||
|
// result = append(result, r)
|
||||||
|
// }
|
||||||
|
// return string(result)
|
||||||
|
// }
|
||||||
|
|
||||||
|
func geocodeAndGetWeather(loc string) (WeatherCity, WeatherForecast, bool) {
|
||||||
|
var city WeatherCity
|
||||||
|
var forecast WeatherForecast
|
||||||
|
// 1. Geocode
|
||||||
|
geoURL := fmt.Sprintf("https://geocoding-api.open-meteo.com/v1/search?name=%s&count=1", urlQueryEscape(loc))
|
||||||
|
resp, err := http.Get(geoURL)
|
||||||
|
if err != nil {
|
||||||
|
return city, forecast, false
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
var geo struct {
|
||||||
|
Results []struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Country string `json:"country"`
|
||||||
|
Lat float64 `json:"latitude"`
|
||||||
|
Lon float64 `json:"longitude"`
|
||||||
|
} `json:"results"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&geo); err != nil || len(geo.Results) == 0 {
|
||||||
|
return city, forecast, false
|
||||||
|
}
|
||||||
|
g := geo.Results[0]
|
||||||
|
city = WeatherCity{
|
||||||
|
Name: g.Name,
|
||||||
|
Country: g.Country,
|
||||||
|
Lat: g.Lat,
|
||||||
|
Lon: g.Lon,
|
||||||
|
}
|
||||||
|
// 2. Weather (current + forecast)
|
||||||
|
weatherURL := fmt.Sprintf("https://api.open-meteo.com/v1/forecast?latitude=%f&longitude=%f¤t=temperature_2m,weather_code,wind_speed_10m,relative_humidity_2m&daily=temperature_2m_min,temperature_2m_max,weather_code&forecast_days=3&timezone=auto", g.Lat, g.Lon)
|
||||||
|
resp2, err := http.Get(weatherURL)
|
||||||
|
if err != nil {
|
||||||
|
return city, forecast, false
|
||||||
|
}
|
||||||
|
defer resp2.Body.Close()
|
||||||
|
var data struct {
|
||||||
|
Current struct {
|
||||||
|
Temp float64 `json:"temperature_2m"`
|
||||||
|
Wind float64 `json:"wind_speed_10m"`
|
||||||
|
Hum int `json:"relative_humidity_2m"`
|
||||||
|
Code int `json:"weather_code"`
|
||||||
|
} `json:"current"`
|
||||||
|
Daily struct {
|
||||||
|
Dates []string `json:"time"`
|
||||||
|
MinTemp []float64 `json:"temperature_2m_min"`
|
||||||
|
MaxTemp []float64 `json:"temperature_2m_max"`
|
||||||
|
Weather []int `json:"weather_code"`
|
||||||
|
} `json:"daily"`
|
||||||
|
}
|
||||||
|
body, _ := io.ReadAll(resp2.Body)
|
||||||
|
if err := json.Unmarshal(body, &data); err != nil {
|
||||||
|
return city, forecast, false
|
||||||
|
}
|
||||||
|
forecast.Current = WeatherCurrent{
|
||||||
|
Temperature: data.Current.Temp,
|
||||||
|
Wind: data.Current.Wind,
|
||||||
|
Humidity: data.Current.Hum,
|
||||||
|
Condition: weatherDescription(data.Current.Code),
|
||||||
|
}
|
||||||
|
for i := range data.Daily.Dates {
|
||||||
|
forecast.Forecast = append(forecast.Forecast, WeatherDay{
|
||||||
|
Date: data.Daily.Dates[i],
|
||||||
|
MinTemp: data.Daily.MinTemp[i],
|
||||||
|
MaxTemp: data.Daily.MaxTemp[i],
|
||||||
|
Condition: weatherDescription(data.Daily.Weather[i]),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return city, forecast, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func weatherDescription(code int) string {
|
||||||
|
// Minimal mapping, can be expanded
|
||||||
|
switch code {
|
||||||
|
case 0:
|
||||||
|
return "Clear"
|
||||||
|
case 1, 2, 3:
|
||||||
|
return "Partly cloudy"
|
||||||
|
case 45, 48:
|
||||||
|
return "Fog"
|
||||||
|
case 51, 53, 55, 56, 57:
|
||||||
|
return "Drizzle"
|
||||||
|
case 61, 63, 65, 66, 67, 80, 81, 82:
|
||||||
|
return "Rain"
|
||||||
|
case 71, 73, 75, 77, 85, 86:
|
||||||
|
return "Snow"
|
||||||
|
case 95, 96, 99:
|
||||||
|
return "Thunderstorm"
|
||||||
|
default:
|
||||||
|
return "Unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper for safe query escaping
|
||||||
|
func urlQueryEscape(s string) string {
|
||||||
|
return strings.ReplaceAll(strings.ReplaceAll(s, " ", "+"), "%", "")
|
||||||
|
}
|
|
@ -118,6 +118,151 @@
|
||||||
background-color: #27ae60;
|
background-color: #27ae60;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.weather {
|
||||||
|
padding: 0;
|
||||||
|
background: none;
|
||||||
|
border-radius: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
color: var(--snip-text);
|
||||||
|
min-width: 0;
|
||||||
|
max-width: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 1.13em;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-location {
|
||||||
|
opacity: 0.82;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-temp {
|
||||||
|
font-size: 2.3em;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--blue); /* not sure if using "var(--blue);" is the right idea */
|
||||||
|
letter-spacing: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-deg {
|
||||||
|
font-size: 0.5em;
|
||||||
|
font-weight: 500;
|
||||||
|
vertical-align: super;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-current {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 12px 14px 10px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-icon {
|
||||||
|
font-size: 1.7em;
|
||||||
|
margin-right: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-cond {
|
||||||
|
font-size: 1.09em;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--highlight);
|
||||||
|
margin-right: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-details {
|
||||||
|
font-size: 0.97em;
|
||||||
|
opacity: 0.82;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-details span {
|
||||||
|
color: var(--fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-forecast {
|
||||||
|
display: flex;
|
||||||
|
gap: 7px;
|
||||||
|
margin-top: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-forecast-day {
|
||||||
|
flex: 1 1 0;
|
||||||
|
min-width: 0;
|
||||||
|
background: var(--search-bg);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 6px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--font-fg);
|
||||||
|
box-shadow: 0 1px 3px color-mix(in srgb, var(--search-bg), black 15%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-forecast-date {
|
||||||
|
font-size: 0.97em;
|
||||||
|
font-weight: 500;
|
||||||
|
opacity: 0.82;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
color: var(--highlight);
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-forecast-temps {
|
||||||
|
font-size: 1.13em;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-forecast-cond {
|
||||||
|
font-size: 0.98em;
|
||||||
|
opacity: 0.83;
|
||||||
|
color: var(--fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.weather-current {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-current-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-icon {
|
||||||
|
font-size: 1.7em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-cond {
|
||||||
|
font-size: 1.08em;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--highlight);
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-current-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-detail {
|
||||||
|
font-size: 0.98em;
|
||||||
|
color: var(--blue);
|
||||||
|
opacity: 0.85;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
/* Responsive adjustments */
|
/* Responsive adjustments */
|
||||||
@media only screen and (max-width: 1220px) {
|
@media only screen and (max-width: 1220px) {
|
||||||
|
@ -136,4 +281,14 @@
|
||||||
width: 90%;
|
width: 90%;
|
||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 540px) {
|
||||||
|
.weather-forecast {
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.weather-forecast-day {
|
||||||
|
padding: 6px 2px;
|
||||||
|
font-size: 0.95em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -168,8 +168,8 @@
|
||||||
{{if .Instant}}
|
{{if .Instant}}
|
||||||
<div class="instant-container">
|
<div class="instant-container">
|
||||||
<div class="instant-box">
|
<div class="instant-box">
|
||||||
<h3>{{.Instant.Title}}</h3>
|
|
||||||
{{if eq .Instant.Type "currency"}}
|
{{if eq .Instant.Type "currency"}}
|
||||||
|
<h3>{{.Instant.Title}}</h3> <!-- TODO: improve looks and feels of this ui -->
|
||||||
<div class="instant-result">
|
<div class="instant-result">
|
||||||
{{index .Instant.Content "display"}}
|
{{index .Instant.Content "display"}}
|
||||||
</div>
|
</div>
|
||||||
|
@ -208,7 +208,50 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
{{if eq .Instant.Type "weather"}}
|
||||||
|
<div class="weather">
|
||||||
|
<div class="weather-header">
|
||||||
|
<span class="weather-location">
|
||||||
|
{{index .Instant.Content "city"}}, {{index .Instant.Content "country"}}
|
||||||
|
</span>
|
||||||
|
<span class="weather-temp">
|
||||||
|
{{with (index .Instant.Content "current")}}{{printf "%.0f" .Temperature}}{{end}}°
|
||||||
|
<span class="weather-deg">C</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="weather-current">
|
||||||
|
<div class="weather-current-left">
|
||||||
|
<span class="weather-icon">
|
||||||
|
{{weatherIcon (index .Instant.Content "current")}}
|
||||||
|
</span>
|
||||||
|
<span class="weather-cond">
|
||||||
|
{{with (index .Instant.Content "current")}}{{.Condition}}{{end}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="weather-current-right">
|
||||||
|
<span class="weather-detail" title="Humidity">
|
||||||
|
💧 {{with (index .Instant.Content "current")}}{{.Humidity}}{{end}}%
|
||||||
|
</span>
|
||||||
|
<span class="weather-detail" title="Wind">
|
||||||
|
💨 {{with (index .Instant.Content "current")}}{{printf "%.1f" .Wind}}{{end}} m/s
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="weather-forecast">
|
||||||
|
{{range $i, $day := (index .Instant.Content "forecast")}}
|
||||||
|
<div class="weather-forecast-day">
|
||||||
|
<div class="weather-forecast-date">{{formatShortDate $day.Date}}</div>
|
||||||
|
<div class="weather-forecast-temps">
|
||||||
|
<span>{{printf "%.0f" $day.MinTemp}}–{{printf "%.0f" $day.MaxTemp}}°C</span>
|
||||||
|
</div>
|
||||||
|
<div class="weather-forecast-cond">{{$day.Condition}}</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
{{if eq .Instant.Type "wiki"}}
|
{{if eq .Instant.Type "wiki"}}
|
||||||
|
<h3>{{.Instant.Title}}</h3>
|
||||||
<div class="instant-result">{{index .Instant.Content "text"}}</div>
|
<div class="instant-result">{{index .Instant.Content "text"}}</div>
|
||||||
<div><a href="{{index .Instant.Content "link"}}" target="_blank">Read more on Wikipedia →</a></div>
|
<div><a href="{{index .Instant.Content "link"}}" target="_blank">Read more on Wikipedia →</a></div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue