mirror of
https://github.com/glanceapp/glance.git
synced 2025-06-21 18:31:24 +02:00
Merge branch 'main' into dev
This commit is contained in:
commit
7f0e9b3289
@ -399,7 +399,7 @@ pages:
|
|||||||
| show-mobile-header | boolean | no | false |
|
| show-mobile-header | boolean | no | false |
|
||||||
| columns | array | yes | |
|
| columns | array | yes | |
|
||||||
|
|
||||||
#### `title`
|
#### `name`
|
||||||
The name of the page which gets shown in the navigation bar.
|
The name of the page which gets shown in the navigation bar.
|
||||||
|
|
||||||
#### `slug`
|
#### `slug`
|
||||||
@ -1340,6 +1340,7 @@ Examples:
|
|||||||
| body | any | no | |
|
| body | any | no | |
|
||||||
| frameless | boolean | no | false |
|
| frameless | boolean | no | false |
|
||||||
| allow-insecure | boolean | no | false |
|
| allow-insecure | boolean | no | false |
|
||||||
|
| skip-json-validation | boolean | no | false |
|
||||||
| template | string | yes | |
|
| template | string | yes | |
|
||||||
| parameters | key (string) & value (string|array) | no | |
|
| parameters | key (string) & value (string|array) | no | |
|
||||||
| subrequests | map of requests | no | |
|
| subrequests | map of requests | no | |
|
||||||
@ -1387,6 +1388,9 @@ When set to `true`, removes the border and padding around the widget.
|
|||||||
##### `allow-insecure`
|
##### `allow-insecure`
|
||||||
Whether to ignore invalid/self-signed certificates.
|
Whether to ignore invalid/self-signed certificates.
|
||||||
|
|
||||||
|
##### `skip-json-validation`
|
||||||
|
When set to `true`, skips the JSON validation step. This is useful when the API returns JSON Lines/newline-delimited JSON, which is a format that consists of several JSON objects separated by newlines.
|
||||||
|
|
||||||
##### `template`
|
##### `template`
|
||||||
The template that will be used to display the data. It relies on Go's `html/template` package so it's recommended to go through [its documentation](https://pkg.go.dev/text/template) to understand how to do basic things such as conditionals, loops, etc. In addition, it also uses [tidwall's gjson](https://github.com/tidwall/gjson) package to parse the JSON data so it's worth going through its documentation if you want to use more advanced JSON selectors. You can view additional examples with explanations and function definitions [here](custom-api.md).
|
The template that will be used to display the data. It relies on Go's `html/template` package so it's recommended to go through [its documentation](https://pkg.go.dev/text/template) to understand how to do basic things such as conditionals, loops, etc. In addition, it also uses [tidwall's gjson](https://github.com/tidwall/gjson) package to parse the JSON data so it's worth going through its documentation if you want to use more advanced JSON selectors. You can view additional examples with explanations and function definitions [here](custom-api.md).
|
||||||
|
|
||||||
|
@ -226,10 +226,10 @@ JSON response:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Calculations can be performed, however all numbers must be converted to floats first if they are not already:
|
Calculations can be performed on either ints or floats. If both numbers are ints, an int will be returned, otherwise a float will be returned. If you try to divide by zero, 0 will be returned. If you provide non-numeric values, `NaN` will be returned.
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<div>{{ sub (.JSON.Int "price" | toFloat) (.JSON.Int "discount" | toFloat) }}</div>
|
<div>{{ sub (.JSON.Int "price") (.JSON.Int "discount") }}</div>
|
||||||
```
|
```
|
||||||
|
|
||||||
Output:
|
Output:
|
||||||
@ -309,6 +309,55 @@ You can also access the response headers:
|
|||||||
<div>{{ .Response.Header.Get "Content-Type" }}</div>
|
<div>{{ .Response.Header.Get "Content-Type" }}</div>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
JSON response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"name": "Steve", "age": 30}
|
||||||
|
{"name": "Alex", "age": 25}
|
||||||
|
{"name": "John", "age": 35}
|
||||||
|
```
|
||||||
|
|
||||||
|
The above format is "[ndjson](https://docs.mulesoft.com/dataweave/latest/dataweave-formats-ndjson)" or "[JSON Lines](https://jsonlines.org/)", where each line is a separate JSON object. To parse this format, you must first disable the JSON validation check in your config, since by default the response is expected to be a single valid JSON object:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- type: custom-api
|
||||||
|
skip-json-validation: true
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, to iterate over each object you can use `.JSONLines`:
|
||||||
|
|
||||||
|
```html
|
||||||
|
{{ range .JSONLines }}
|
||||||
|
<p>{{ .String "name" }} is {{ .Int "age" }} years old</p>
|
||||||
|
{{ end }}
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<p>Steve is 30 years old</p>
|
||||||
|
<p>Alex is 25 years old</p>
|
||||||
|
<p>John is 35 years old</p>
|
||||||
|
```
|
||||||
|
|
||||||
|
For other ways of selecting data from a JSON Lines response, have a look at the docs for [tidwall/gjson](https://github.com/tidwall/gjson/tree/master?tab=readme-ov-file#json-lines). For example, to get an array of all names, you can use the following:
|
||||||
|
|
||||||
|
```html
|
||||||
|
{{ range .JSON.Array "..#.name" }}
|
||||||
|
<p>{{ .String "" }}</p>
|
||||||
|
{{ end }}
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<p>Steve</p>
|
||||||
|
<p>Alex</p>
|
||||||
|
<p>John</p>
|
||||||
|
```
|
||||||
|
|
||||||
## Functions
|
## Functions
|
||||||
|
|
||||||
The following functions are available on the `JSON` object:
|
The following functions are available on the `JSON` object:
|
||||||
@ -325,6 +374,9 @@ The following helper functions provided by Glance are available:
|
|||||||
- `toFloat(i int) float`: Converts an integer to a float.
|
- `toFloat(i int) float`: Converts an integer to a float.
|
||||||
- `toInt(f float) int`: Converts a float to an integer.
|
- `toInt(f float) int`: Converts a float to an integer.
|
||||||
- `toRelativeTime(t time.Time) template.HTMLAttr`: Converts Time to a relative time such as 2h, 1d, etc which dynamically updates. **NOTE:** the value of this function should be used as an attribute in an HTML tag, e.g. `<span {{ toRelativeTime .Time }}></span>`.
|
- `toRelativeTime(t time.Time) template.HTMLAttr`: Converts Time to a relative time such as 2h, 1d, etc which dynamically updates. **NOTE:** the value of this function should be used as an attribute in an HTML tag, e.g. `<span {{ toRelativeTime .Time }}></span>`.
|
||||||
|
- `now() time.Time`: Returns the current time.
|
||||||
|
- `offsetNow(offset string) time.Time`: Returns the current time with an offset. The offset can be positive or negative and must be in the format "3h" "-1h" or "2h30m10s".
|
||||||
|
- `duration(str string) time.Duration`: Parses a string such as `1h`, `24h`, `5h30m`, etc into a `time.Duration`.
|
||||||
- `parseTime(layout string, s string) time.Time`: Parses a string into time.Time. The layout must be provided in Go's [date format](https://pkg.go.dev/time#pkg-constants). You can alternatively use these values instead of the literal format: "unix", "RFC3339", "RFC3339Nano", "DateTime", "DateOnly".
|
- `parseTime(layout string, s string) time.Time`: Parses a string into time.Time. The layout must be provided in Go's [date format](https://pkg.go.dev/time#pkg-constants). You can alternatively use these values instead of the literal format: "unix", "RFC3339", "RFC3339Nano", "DateTime", "DateOnly".
|
||||||
- `parseRelativeTime(layout string, s string) time.Time`: A shorthand for `{{ .String "date" | parseTime "rfc3339" | toRelativeTime }}`.
|
- `parseRelativeTime(layout string, s string) time.Time`: A shorthand for `{{ .String "date" | parseTime "rfc3339" | toRelativeTime }}`.
|
||||||
- `add(a, b float) float`: Adds two numbers.
|
- `add(a, b float) float`: Adds two numbers.
|
||||||
@ -343,6 +395,7 @@ The following helper functions provided by Glance are available:
|
|||||||
- `sortByInt(key string, order string, arr []JSON): []JSON`: Sorts an array of JSON objects by an integer key in either ascending or descending order.
|
- `sortByInt(key string, order string, arr []JSON): []JSON`: Sorts an array of JSON objects by an integer key in either ascending or descending order.
|
||||||
- `sortByFloat(key string, order string, arr []JSON): []JSON`: Sorts an array of JSON objects by a float key in either ascending or descending order.
|
- `sortByFloat(key string, order string, arr []JSON): []JSON`: Sorts an array of JSON objects by a float key in either ascending or descending order.
|
||||||
- `sortByTime(key string, layout string, order string, arr []JSON): []JSON`: Sorts an array of JSON objects by a time key in either ascending or descending order. The format must be provided in Go's [date format](https://pkg.go.dev/time#pkg-constants).
|
- `sortByTime(key string, layout string, order string, arr []JSON): []JSON`: Sorts an array of JSON objects by a time key in either ascending or descending order. The format must be provided in Go's [date format](https://pkg.go.dev/time#pkg-constants).
|
||||||
|
- `concat(strings ...string) string`: Concatenates multiple strings together.
|
||||||
|
|
||||||
The following helper functions provided by Go's `text/template` are available:
|
The following helper functions provided by Go's `text/template` are available:
|
||||||
|
|
||||||
|
@ -26,6 +26,9 @@ If you know how to setup an HTTP server and a bit of HTML and CSS you're ready t
|
|||||||
### `Widget-Title`
|
### `Widget-Title`
|
||||||
Used to specify the title of the widget. If not provided, the widget's title will be "Extension".
|
Used to specify the title of the widget. If not provided, the widget's title will be "Extension".
|
||||||
|
|
||||||
|
### `Widget-Title-URL`
|
||||||
|
Used to specify the URL that will be opened when the widget's title is clicked. If the user has specified a `title-url` in their config, it will take precedence over this header.
|
||||||
|
|
||||||
### `Widget-Content-Type`
|
### `Widget-Content-Type`
|
||||||
Used to specify the content type that will be returned by the extension. If not provided, the content will be shown as plain text.
|
Used to specify the content type that will be returned by the extension. If not provided, the content will be shown as plain text.
|
||||||
|
|
||||||
|
@ -27,6 +27,9 @@ var globalTemplateFunctions = template.FuncMap{
|
|||||||
"formatPrice": func(price float64) string {
|
"formatPrice": func(price float64) string {
|
||||||
return intl.Sprintf("%.2f", price)
|
return intl.Sprintf("%.2f", price)
|
||||||
},
|
},
|
||||||
|
"formatPriceWithPrecision": func(precision int, price float64) string {
|
||||||
|
return intl.Sprintf("%."+strconv.Itoa(precision)+"f", price)
|
||||||
|
},
|
||||||
"dynamicRelativeTimeAttrs": dynamicRelativeTimeAttrs,
|
"dynamicRelativeTimeAttrs": dynamicRelativeTimeAttrs,
|
||||||
"formatServerMegabytes": func(mb uint64) template.HTML {
|
"formatServerMegabytes": func(mb uint64) template.HTML {
|
||||||
var value string
|
var value string
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
<div class="market-values shrink-0">
|
<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="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>
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
@ -25,15 +25,16 @@ var customAPIWidgetTemplate = mustParseTemplate("custom-api.html", "widget-base.
|
|||||||
|
|
||||||
// Needs to be exported for the YAML unmarshaler to work
|
// Needs to be exported for the YAML unmarshaler to work
|
||||||
type CustomAPIRequest struct {
|
type CustomAPIRequest struct {
|
||||||
URL string `yaml:"url"`
|
URL string `yaml:"url"`
|
||||||
AllowInsecure bool `yaml:"allow-insecure"`
|
AllowInsecure bool `yaml:"allow-insecure"`
|
||||||
Headers map[string]string `yaml:"headers"`
|
Headers map[string]string `yaml:"headers"`
|
||||||
Parameters queryParametersField `yaml:"parameters"`
|
Parameters queryParametersField `yaml:"parameters"`
|
||||||
Method string `yaml:"method"`
|
Method string `yaml:"method"`
|
||||||
BodyType string `yaml:"body-type"`
|
BodyType string `yaml:"body-type"`
|
||||||
Body any `yaml:"body"`
|
Body any `yaml:"body"`
|
||||||
bodyReader io.ReadSeeker `yaml:"-"`
|
SkipJSONValidation bool `yaml:"skip-json-validation"`
|
||||||
httpRequest *http.Request `yaml:"-"`
|
bodyReader io.ReadSeeker `yaml:"-"`
|
||||||
|
httpRequest *http.Request `yaml:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type customAPIWidget struct {
|
type customAPIWidget struct {
|
||||||
@ -157,6 +158,17 @@ type customAPITemplateData struct {
|
|||||||
subrequests map[string]*customAPIResponseData
|
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 {
|
func (data *customAPITemplateData) Subrequest(key string) *customAPIResponseData {
|
||||||
req, exists := data.subrequests[key]
|
req, exists := data.subrequests[key]
|
||||||
if !exists {
|
if !exists {
|
||||||
@ -190,7 +202,7 @@ func fetchCustomAPIRequest(ctx context.Context, req *CustomAPIRequest) (*customA
|
|||||||
|
|
||||||
body := strings.TrimSpace(string(bodyBytes))
|
body := strings.TrimSpace(string(bodyBytes))
|
||||||
|
|
||||||
if body != "" && !gjson.Valid(body) {
|
if !req.SkipJSONValidation && body != "" && !gjson.Valid(body) {
|
||||||
truncatedBody, isTruncated := limitStringLength(body, 100)
|
truncatedBody, isTruncated := limitStringLength(body, 100)
|
||||||
if isTruncated {
|
if isTruncated {
|
||||||
truncatedBody += "... <truncated>"
|
truncatedBody += "... <truncated>"
|
||||||
@ -342,6 +354,23 @@ func (r *decoratedGJSONResult) Bool(key string) bool {
|
|||||||
return r.Get(key).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 customAPITemplateFuncs = func() template.FuncMap {
|
||||||
var regexpCacheMu sync.Mutex
|
var regexpCacheMu sync.Mutex
|
||||||
var regexpCache = make(map[string]*regexp.Regexp)
|
var regexpCache = make(map[string]*regexp.Regexp)
|
||||||
@ -359,6 +388,31 @@ var customAPITemplateFuncs = func() template.FuncMap {
|
|||||||
return regex
|
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{
|
funcs := template.FuncMap{
|
||||||
"toFloat": func(a int) float64 {
|
"toFloat": func(a int) float64 {
|
||||||
return float64(a)
|
return float64(a)
|
||||||
@ -366,21 +420,35 @@ var customAPITemplateFuncs = func() template.FuncMap {
|
|||||||
"toInt": func(a float64) int {
|
"toInt": func(a float64) int {
|
||||||
return int(a)
|
return int(a)
|
||||||
},
|
},
|
||||||
"add": func(a, b float64) float64 {
|
"add": func(a, b any) any {
|
||||||
return a + b
|
return doMathOpWithAny(a, b, "add")
|
||||||
},
|
},
|
||||||
"sub": func(a, b float64) float64 {
|
"sub": func(a, b any) any {
|
||||||
return a - b
|
return doMathOpWithAny(a, b, "sub")
|
||||||
},
|
},
|
||||||
"mul": func(a, b float64) float64 {
|
"mul": func(a, b any) any {
|
||||||
return a * b
|
return doMathOpWithAny(a, b, "mul")
|
||||||
},
|
},
|
||||||
"div": func(a, b float64) float64 {
|
"div": func(a, b any) any {
|
||||||
if b == 0 {
|
return doMathOpWithAny(a, b, "div")
|
||||||
return math.NaN()
|
},
|
||||||
|
"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,
|
"parseTime": customAPIFuncParseTime,
|
||||||
"toRelativeTime": dynamicRelativeTimeAttrs,
|
"toRelativeTime": dynamicRelativeTimeAttrs,
|
||||||
@ -465,6 +533,9 @@ var customAPITemplateFuncs = func() template.FuncMap {
|
|||||||
|
|
||||||
return results
|
return results
|
||||||
},
|
},
|
||||||
|
"concat": func(items ...string) string {
|
||||||
|
return strings.Join(items, "")
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for key, value := range globalTemplateFunctions {
|
for key, value := range globalTemplateFunctions {
|
||||||
|
@ -59,6 +59,10 @@ func (widget *extensionWidget) update(ctx context.Context) {
|
|||||||
widget.Title = extension.Title
|
widget.Title = extension.Title
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if widget.TitleURL == "" && extension.TitleURL != "" {
|
||||||
|
widget.TitleURL = extension.TitleURL
|
||||||
|
}
|
||||||
|
|
||||||
widget.cachedHTML = widget.renderTemplate(widget, extensionWidgetTemplate)
|
widget.cachedHTML = widget.renderTemplate(widget, extensionWidgetTemplate)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,8 +73,8 @@ func (widget *extensionWidget) Render() template.HTML {
|
|||||||
type extensionType int
|
type extensionType int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
extensionContentHTML extensionType = iota
|
extensionContentHTML extensionType = iota
|
||||||
extensionContentUnknown = iota
|
extensionContentUnknown
|
||||||
)
|
)
|
||||||
|
|
||||||
var extensionStringToType = map[string]extensionType{
|
var extensionStringToType = map[string]extensionType{
|
||||||
@ -79,6 +83,7 @@ var extensionStringToType = map[string]extensionType{
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
extensionHeaderTitle = "Widget-Title"
|
extensionHeaderTitle = "Widget-Title"
|
||||||
|
extensionHeaderTitleURL = "Widget-Title-URL"
|
||||||
extensionHeaderContentType = "Widget-Content-Type"
|
extensionHeaderContentType = "Widget-Content-Type"
|
||||||
extensionHeaderContentFrameless = "Widget-Content-Frameless"
|
extensionHeaderContentFrameless = "Widget-Content-Frameless"
|
||||||
)
|
)
|
||||||
@ -93,6 +98,7 @@ type extensionRequestOptions struct {
|
|||||||
|
|
||||||
type extension struct {
|
type extension struct {
|
||||||
Title string
|
Title string
|
||||||
|
TitleURL string
|
||||||
Content template.HTML
|
Content template.HTML
|
||||||
Frameless bool
|
Frameless bool
|
||||||
}
|
}
|
||||||
@ -142,6 +148,10 @@ func fetchExtension(options extensionRequestOptions) (extension, error) {
|
|||||||
extension.Title = response.Header.Get(extensionHeaderTitle)
|
extension.Title = response.Header.Get(extensionHeaderTitle)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if response.Header.Get(extensionHeaderTitleURL) != "" {
|
||||||
|
extension.TitleURL = response.Header.Get(extensionHeaderTitleURL)
|
||||||
|
}
|
||||||
|
|
||||||
contentType, ok := extensionStringToType[response.Header.Get(extensionHeaderContentType)]
|
contentType, ok := extensionStringToType[response.Header.Get(extensionHeaderContentType)]
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@ -79,6 +79,7 @@ type market struct {
|
|||||||
Name string
|
Name string
|
||||||
Currency string
|
Currency string
|
||||||
Price float64
|
Price float64
|
||||||
|
PriceHint int
|
||||||
PercentChange float64
|
PercentChange float64
|
||||||
SvgChartPoints string
|
SvgChartPoints string
|
||||||
}
|
}
|
||||||
@ -106,6 +107,7 @@ type marketResponseJson struct {
|
|||||||
RegularMarketPrice float64 `json:"regularMarketPrice"`
|
RegularMarketPrice float64 `json:"regularMarketPrice"`
|
||||||
ChartPreviousClose float64 `json:"chartPreviousClose"`
|
ChartPreviousClose float64 `json:"chartPreviousClose"`
|
||||||
ShortName string `json:"shortName"`
|
ShortName string `json:"shortName"`
|
||||||
|
PriceHint int `json:"priceHint"`
|
||||||
} `json:"meta"`
|
} `json:"meta"`
|
||||||
Indicators struct {
|
Indicators struct {
|
||||||
Quote []struct {
|
Quote []struct {
|
||||||
@ -152,13 +154,14 @@ func fetchMarketsDataFromYahoo(marketRequests []marketRequest) (marketList, erro
|
|||||||
continue
|
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 {
|
if len(prices) > marketChartDays {
|
||||||
prices = prices[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 {
|
if len(prices) >= 2 && prices[len(prices)-2] != 0 {
|
||||||
previous = prices[len(prices)-2]
|
previous = prices[len(prices)-2]
|
||||||
@ -166,21 +169,22 @@ func fetchMarketsDataFromYahoo(marketRequests []marketRequest) (marketList, erro
|
|||||||
|
|
||||||
points := svgPolylineCoordsFromYValues(100, 50, maybeCopySliceWithoutZeroValues(prices))
|
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 {
|
if !exists {
|
||||||
currency = response.Chart.Result[0].Meta.Currency
|
currency = result.Meta.Currency
|
||||||
}
|
}
|
||||||
|
|
||||||
markets = append(markets, market{
|
markets = append(markets, market{
|
||||||
marketRequest: marketRequests[i],
|
marketRequest: marketRequests[i],
|
||||||
Price: response.Chart.Result[0].Meta.RegularMarketPrice,
|
Price: result.Meta.RegularMarketPrice,
|
||||||
Currency: currency,
|
Currency: currency,
|
||||||
|
PriceHint: result.Meta.PriceHint,
|
||||||
Name: ternary(marketRequests[i].CustomName == "",
|
Name: ternary(marketRequests[i].CustomName == "",
|
||||||
response.Chart.Result[0].Meta.ShortName,
|
result.Meta.ShortName,
|
||||||
marketRequests[i].CustomName,
|
marketRequests[i].CustomName,
|
||||||
),
|
),
|
||||||
PercentChange: percentChange(
|
PercentChange: percentChange(
|
||||||
response.Chart.Result[0].Meta.RegularMarketPrice,
|
result.Meta.RegularMarketPrice,
|
||||||
previous,
|
previous,
|
||||||
),
|
),
|
||||||
SvgChartPoints: points,
|
SvgChartPoints: points,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user