Merge branch 'main' into dev

This commit is contained in:
Svilen Markov
2025-03-29 18:01:15 +00:00
8 changed files with 181 additions and 33 deletions

View File

@@ -27,6 +27,9 @@ var globalTemplateFunctions = template.FuncMap{
"formatPrice": func(price float64) string {
return intl.Sprintf("%.2f", price)
},
"formatPriceWithPrecision": func(precision int, price float64) string {
return intl.Sprintf("%."+strconv.Itoa(precision)+"f", price)
},
"dynamicRelativeTimeAttrs": dynamicRelativeTimeAttrs,
"formatServerMegabytes": func(mb uint64) template.HTML {
var value string

View File

@@ -17,7 +17,7 @@
<div class="market-values shrink-0">
<div class="size-h3 text-right {{ if eq .PercentChange 0.0 }}{{ else if gt .PercentChange 0.0 }}color-positive{{ else }}color-negative{{ end }}">{{ printf "%+.2f" .PercentChange }}%</div>
<div class="text-right">{{ .Currency }}{{ .Price | formatPrice }}</div>
<div class="text-right">{{ .Currency }}{{ .Price | formatPriceWithPrecision .PriceHint }}</div>
</div>
</div>
{{ end }}

View File

@@ -25,15 +25,16 @@ var customAPIWidgetTemplate = mustParseTemplate("custom-api.html", "widget-base.
// Needs to be exported for the YAML unmarshaler to work
type CustomAPIRequest struct {
URL string `yaml:"url"`
AllowInsecure bool `yaml:"allow-insecure"`
Headers map[string]string `yaml:"headers"`
Parameters queryParametersField `yaml:"parameters"`
Method string `yaml:"method"`
BodyType string `yaml:"body-type"`
Body any `yaml:"body"`
bodyReader io.ReadSeeker `yaml:"-"`
httpRequest *http.Request `yaml:"-"`
URL string `yaml:"url"`
AllowInsecure bool `yaml:"allow-insecure"`
Headers map[string]string `yaml:"headers"`
Parameters queryParametersField `yaml:"parameters"`
Method string `yaml:"method"`
BodyType string `yaml:"body-type"`
Body any `yaml:"body"`
SkipJSONValidation bool `yaml:"skip-json-validation"`
bodyReader io.ReadSeeker `yaml:"-"`
httpRequest *http.Request `yaml:"-"`
}
type customAPIWidget struct {
@@ -157,6 +158,17 @@ type customAPITemplateData struct {
subrequests map[string]*customAPIResponseData
}
func (data *customAPITemplateData) JSONLines() []decoratedGJSONResult {
result := make([]decoratedGJSONResult, 0, 5)
gjson.ForEachLine(data.JSON.Raw, func(line gjson.Result) bool {
result = append(result, decoratedGJSONResult{line})
return true
})
return result
}
func (data *customAPITemplateData) Subrequest(key string) *customAPIResponseData {
req, exists := data.subrequests[key]
if !exists {
@@ -190,7 +202,7 @@ func fetchCustomAPIRequest(ctx context.Context, req *CustomAPIRequest) (*customA
body := strings.TrimSpace(string(bodyBytes))
if body != "" && !gjson.Valid(body) {
if !req.SkipJSONValidation && body != "" && !gjson.Valid(body) {
truncatedBody, isTruncated := limitStringLength(body, 100)
if isTruncated {
truncatedBody += "... <truncated>"
@@ -342,6 +354,23 @@ func (r *decoratedGJSONResult) Bool(key string) bool {
return r.Get(key).Bool()
}
func customAPIDoMathOp[T int | float64](a, b T, op string) T {
switch op {
case "add":
return a + b
case "sub":
return a - b
case "mul":
return a * b
case "div":
if b == 0 {
return 0
}
return a / b
}
return 0
}
var customAPITemplateFuncs = func() template.FuncMap {
var regexpCacheMu sync.Mutex
var regexpCache = make(map[string]*regexp.Regexp)
@@ -359,6 +388,31 @@ var customAPITemplateFuncs = func() template.FuncMap {
return regex
}
doMathOpWithAny := func(a, b any, op string) any {
switch at := a.(type) {
case int:
switch bt := b.(type) {
case int:
return customAPIDoMathOp(at, bt, op)
case float64:
return customAPIDoMathOp(float64(at), bt, op)
default:
return math.NaN()
}
case float64:
switch bt := b.(type) {
case int:
return customAPIDoMathOp(at, float64(bt), op)
case float64:
return customAPIDoMathOp(at, bt, op)
default:
return math.NaN()
}
default:
return math.NaN()
}
}
funcs := template.FuncMap{
"toFloat": func(a int) float64 {
return float64(a)
@@ -366,21 +420,35 @@ var customAPITemplateFuncs = func() template.FuncMap {
"toInt": func(a float64) int {
return int(a)
},
"add": func(a, b float64) float64 {
return a + b
"add": func(a, b any) any {
return doMathOpWithAny(a, b, "add")
},
"sub": func(a, b float64) float64 {
return a - b
"sub": func(a, b any) any {
return doMathOpWithAny(a, b, "sub")
},
"mul": func(a, b float64) float64 {
return a * b
"mul": func(a, b any) any {
return doMathOpWithAny(a, b, "mul")
},
"div": func(a, b float64) float64 {
if b == 0 {
return math.NaN()
"div": func(a, b any) any {
return doMathOpWithAny(a, b, "div")
},
"now": func() time.Time {
return time.Now()
},
"offsetNow": func(offset string) time.Time {
d, err := time.ParseDuration(offset)
if err != nil {
return time.Now()
}
return time.Now().Add(d)
},
"duration": func(str string) time.Duration {
d, err := time.ParseDuration(str)
if err != nil {
return 0
}
return a / b
return d
},
"parseTime": customAPIFuncParseTime,
"toRelativeTime": dynamicRelativeTimeAttrs,
@@ -465,6 +533,9 @@ var customAPITemplateFuncs = func() template.FuncMap {
return results
},
"concat": func(items ...string) string {
return strings.Join(items, "")
},
}
for key, value := range globalTemplateFunctions {

View File

@@ -59,6 +59,10 @@ func (widget *extensionWidget) update(ctx context.Context) {
widget.Title = extension.Title
}
if widget.TitleURL == "" && extension.TitleURL != "" {
widget.TitleURL = extension.TitleURL
}
widget.cachedHTML = widget.renderTemplate(widget, extensionWidgetTemplate)
}
@@ -69,8 +73,8 @@ func (widget *extensionWidget) Render() template.HTML {
type extensionType int
const (
extensionContentHTML extensionType = iota
extensionContentUnknown = iota
extensionContentHTML extensionType = iota
extensionContentUnknown
)
var extensionStringToType = map[string]extensionType{
@@ -79,6 +83,7 @@ var extensionStringToType = map[string]extensionType{
const (
extensionHeaderTitle = "Widget-Title"
extensionHeaderTitleURL = "Widget-Title-URL"
extensionHeaderContentType = "Widget-Content-Type"
extensionHeaderContentFrameless = "Widget-Content-Frameless"
)
@@ -93,6 +98,7 @@ type extensionRequestOptions struct {
type extension struct {
Title string
TitleURL string
Content template.HTML
Frameless bool
}
@@ -142,6 +148,10 @@ func fetchExtension(options extensionRequestOptions) (extension, error) {
extension.Title = response.Header.Get(extensionHeaderTitle)
}
if response.Header.Get(extensionHeaderTitleURL) != "" {
extension.TitleURL = response.Header.Get(extensionHeaderTitleURL)
}
contentType, ok := extensionStringToType[response.Header.Get(extensionHeaderContentType)]
if !ok {

View File

@@ -79,6 +79,7 @@ type market struct {
Name string
Currency string
Price float64
PriceHint int
PercentChange float64
SvgChartPoints string
}
@@ -106,6 +107,7 @@ type marketResponseJson struct {
RegularMarketPrice float64 `json:"regularMarketPrice"`
ChartPreviousClose float64 `json:"chartPreviousClose"`
ShortName string `json:"shortName"`
PriceHint int `json:"priceHint"`
} `json:"meta"`
Indicators struct {
Quote []struct {
@@ -152,13 +154,14 @@ func fetchMarketsDataFromYahoo(marketRequests []marketRequest) (marketList, erro
continue
}
prices := response.Chart.Result[0].Indicators.Quote[0].Close
result := &response.Chart.Result[0]
prices := result.Indicators.Quote[0].Close
if len(prices) > marketChartDays {
prices = prices[len(prices)-marketChartDays:]
}
previous := response.Chart.Result[0].Meta.RegularMarketPrice
previous := result.Meta.RegularMarketPrice
if len(prices) >= 2 && prices[len(prices)-2] != 0 {
previous = prices[len(prices)-2]
@@ -166,21 +169,22 @@ func fetchMarketsDataFromYahoo(marketRequests []marketRequest) (marketList, erro
points := svgPolylineCoordsFromYValues(100, 50, maybeCopySliceWithoutZeroValues(prices))
currency, exists := currencyToSymbol[strings.ToUpper(response.Chart.Result[0].Meta.Currency)]
currency, exists := currencyToSymbol[strings.ToUpper(result.Meta.Currency)]
if !exists {
currency = response.Chart.Result[0].Meta.Currency
currency = result.Meta.Currency
}
markets = append(markets, market{
marketRequest: marketRequests[i],
Price: response.Chart.Result[0].Meta.RegularMarketPrice,
Price: result.Meta.RegularMarketPrice,
Currency: currency,
PriceHint: result.Meta.PriceHint,
Name: ternary(marketRequests[i].CustomName == "",
response.Chart.Result[0].Meta.ShortName,
result.Meta.ShortName,
marketRequests[i].CustomName,
),
PercentChange: percentChange(
response.Chart.Result[0].Meta.RegularMarketPrice,
result.Meta.RegularMarketPrice,
previous,
),
SvgChartPoints: points,