mirror of
https://github.com/Xevion/glance.git
synced 2025-12-10 16:07:22 -06:00
Merge branch 'main' into dev
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user