Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f5bfd9d4d1 | ||
|
|
9e3639eb54 | ||
|
|
b4094b28bd | ||
|
|
b294839b79 | ||
|
|
14a21de37f | ||
|
|
d9239acbce | ||
|
|
a2247c0b6c | ||
|
|
c1aaec5ffc | ||
|
|
bcef9fbd61 | ||
|
|
32db59fda2 | ||
|
|
92bc68b61a | ||
|
|
a6382b2e1d | ||
|
|
571cdaf618 | ||
|
|
aa3b2c3b1b | ||
|
|
9bbf73db97 | ||
|
|
dc950e7ec2 | ||
|
|
91ca57e242 | ||
|
|
95541ef5e1 | ||
|
|
1ace129a58 | ||
|
|
af04605d7d | ||
|
|
0ec9cf4aaa | ||
|
|
a5b0664b9c | ||
|
|
c67eb4d2c0 | ||
|
|
e99ee4f774 | ||
|
|
74e6c5c960 | ||
|
|
f801da88a7 | ||
|
|
66dcd1fa34 | ||
|
|
4f9e48cc17 |
@@ -1,6 +1,6 @@
|
||||
<p align="center"><em>What if you could see everything at a...</em></p>
|
||||
<h1 align="center">Glance</h1>
|
||||
<p align="center"><a href="#installation">Install</a> • <a href="docs/configuration.md">Configuration</a> • <a href="https://discord.com/invite/7KQ7Xa9kJd">Discord</a> • <a href="https://github.com/sponsors/glanceapp">Sponsor</a></p>
|
||||
<p align="center"><a href="#installation">Install</a> • <a href="docs/configuration.md#configuring-glance">Configuration</a> • <a href="https://discord.com/invite/7KQ7Xa9kJd">Discord</a> • <a href="https://github.com/sponsors/glanceapp">Sponsor</a></p>
|
||||
<p align="center"><a href="https://github.com/glanceapp/community-widgets">Community widgets</a> • <a href="docs/preconfigured-pages.md">Preconfigured pages</a> • <a href="docs/themes.md">Themes</a></p>
|
||||
|
||||

|
||||
@@ -17,7 +17,7 @@
|
||||
* Docker containers status
|
||||
* Server stats
|
||||
* Custom widgets
|
||||
* [and many more...](docs/configuration.md)
|
||||
* [and many more...](docs/configuration.md#configuring-glance)
|
||||
|
||||
### Fast and lightweight
|
||||
* Low memory usage
|
||||
@@ -46,8 +46,7 @@ Easily create your own theme by tweaking a few numbers or choose from one of the
|
||||
<br>
|
||||
|
||||
## Configuration
|
||||
Configuration is done through YAML files, to learn more about how the layout works, how to add more pages and how to configure widgets, visit the [configuration documentation](docs/configuration.md).
|
||||
|
||||
Configuration is done through YAML files, to learn more about how the layout works, how to add more pages and how to configure widgets, visit the [configuration documentation](docs/configuration.md#configuring-glance).
|
||||
<details>
|
||||
<summary><strong>Preview example configuration file</strong></summary>
|
||||
<br>
|
||||
|
||||
@@ -389,10 +389,12 @@ Example:
|
||||
|
||||
```yaml
|
||||
theme:
|
||||
# This will be the default theme
|
||||
background-color: 100 20 10
|
||||
primary-color: 40 90 40
|
||||
contrast-multiplier: 1.1
|
||||
|
||||
disable-picker: false
|
||||
presets:
|
||||
gruvbox-dark:
|
||||
background-color: 0 0 16
|
||||
@@ -421,6 +423,7 @@ If you don't want to spend time configuring your own theme, there are [several a
|
||||
| contrast-multiplier | number | no | 1 |
|
||||
| text-saturation-multiplier | number | no | 1 |
|
||||
| custom-css-file | string | no | |
|
||||
| disable-picker | bool | false | |
|
||||
| presets | object | no | |
|
||||
|
||||
#### `light`
|
||||
@@ -466,8 +469,11 @@ theme:
|
||||
>
|
||||
> In addition, you can also use the `css-class` property which is available on every widget to set custom class names for individual widgets.
|
||||
|
||||
#### `disable-picker`
|
||||
When set to `true` hides the theme picker and disables the abiltity to switch between themes. All users who previously picked a non-default theme will be switched over to the default theme.
|
||||
|
||||
#### `presets`
|
||||
Define additional theme presets that can be selected from the theme switcher on the page. For each preset, you can specify the same properties as for the default theme, such as `background-color`, `primary-color`, `positive-color`, `negative-color`, `contrast-multiplier`, etc., except for the `custom-css-file` property.
|
||||
Define additional theme presets that can be selected from the theme picker on the page. For each preset, you can specify the same properties as for the default theme, such as `background-color`, `primary-color`, `positive-color`, `negative-color`, `contrast-multiplier`, etc., except for the `custom-css-file` property.
|
||||
|
||||
Example:
|
||||
|
||||
@@ -2141,7 +2147,7 @@ Alternatively, you can also define the values within your `glance.yml` via the `
|
||||
- type: docker-containers
|
||||
containers:
|
||||
container_name_1:
|
||||
title: Container Name
|
||||
name: Container Name
|
||||
description: Description of the container
|
||||
url: https://container.domain.com
|
||||
icon: si:container-icon
|
||||
|
||||
+14
-3
@@ -238,7 +238,7 @@ Output:
|
||||
<div>90</div>
|
||||
```
|
||||
|
||||
Other operations include `add`, `mul`, and `div`.
|
||||
Other operations include `add`, `mul`, `div` and `mod`.
|
||||
|
||||
<hr>
|
||||
|
||||
@@ -415,6 +415,14 @@ The following functions are available on the `JSON` object:
|
||||
- `Array(key string) []JSON`: Returns the value of the key as an array of `JSON` objects.
|
||||
- `Exists(key string) bool`: Returns true if the key exists in the JSON object.
|
||||
|
||||
The following functions are available on the `Options` object:
|
||||
|
||||
- `StringOr(key string, default string) string`: Returns the value of the key as a string, or the default value if the key does not exist.
|
||||
- `IntOr(key string, default int) int`: Returns the value of the key as an integer, or the default value if the key does not exist.
|
||||
- `FloatOr(key string, default float) float`: Returns the value of the key as a float, or the default value if the key does not exist.
|
||||
- `BoolOr(key string, default bool) bool`: Returns the value of the key as a boolean, or the default value if the key does not exist.
|
||||
- `JSON(key string) JSON`: Returns the value of the key as a stringified `JSON` object, or throws an error if the key does not exist.
|
||||
|
||||
The following helper functions provided by Glance are available:
|
||||
|
||||
- `toFloat(i int) float`: Converts an integer to a float.
|
||||
@@ -431,6 +439,7 @@ The following helper functions provided by Glance are available:
|
||||
- `sub(a, b float) float`: Subtracts two numbers.
|
||||
- `mul(a, b float) float`: Multiplies two numbers.
|
||||
- `div(a, b float) float`: Divides two numbers.
|
||||
- `mod(a, b int) int`: Remainder after dividing a by b (a % b).
|
||||
- `formatApproxNumber(n int) string`: Formats a number to be more human-readable, e.g. 1000 -> 1k.
|
||||
- `formatNumber(n float|int) string`: Formats a number with commas, e.g. 1000 -> 1,000.
|
||||
- `trimPrefix(prefix string, str string) string`: Trims the prefix from a string.
|
||||
@@ -447,6 +456,8 @@ The following helper functions provided by Glance are available:
|
||||
- `concat(strings ...string) string`: Concatenates multiple strings together.
|
||||
- `unique(key string, arr []JSON) []JSON`: Returns a unique array of JSON objects based on the given key.
|
||||
- `percentChange(current float, previous float) float`: Calculates the percentage change between two numbers.
|
||||
- `startOfDay(t time.Time) time.Time`: Returns the start of the day for a given time.
|
||||
- `endOfDay(t time.Time) time.Time`: Returns the end of the day for a given time.
|
||||
|
||||
The following helper functions provided by Go's `text/template` are available:
|
||||
|
||||
@@ -456,8 +467,8 @@ The following helper functions provided by Go's `text/template` are available:
|
||||
- `lte(a, b any) bool`: Compares two values for less than or equal to.
|
||||
- `gt(a, b any) bool`: Compares two values for greater than.
|
||||
- `gte(a, b any) bool`: Compares two values for greater than or equal to.
|
||||
- `and(a, b bool) bool`: Returns true if both values are true.
|
||||
- `or(a, b bool) bool`: Returns true if either value is true.
|
||||
- `and(args ...bool) bool`: Returns true if **all** arguments are true; accepts two or more boolean values.
|
||||
- `or(args ...bool) bool`: Returns true if **any** argument is true; accepts two or more boolean values.
|
||||
- `not(a bool) bool`: Returns the opposite of the value.
|
||||
- `index(a any, b int) any`: Returns the value at the specified index of an array.
|
||||
- `len(a any) int`: Returns the length of an array.
|
||||
|
||||
@@ -47,8 +47,10 @@ type config struct {
|
||||
|
||||
Theme struct {
|
||||
themeProperties `yaml:",inline"`
|
||||
CustomCSSFile string `yaml:"custom-css-file"`
|
||||
Presets orderedYAMLMap[string, *themeProperties] `yaml:"presets"`
|
||||
CustomCSSFile string `yaml:"custom-css-file"`
|
||||
|
||||
DisablePicker bool `yaml:"disable-picker"`
|
||||
Presets orderedYAMLMap[string, *themeProperties] `yaml:"presets"`
|
||||
} `yaml:"theme"`
|
||||
|
||||
Branding struct {
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const httpTestRequestTimeout = 10 * time.Second
|
||||
const httpTestRequestTimeout = 15 * time.Second
|
||||
|
||||
var diagnosticSteps = []diagnosticStep{
|
||||
{
|
||||
@@ -75,7 +75,9 @@ var diagnosticSteps = []diagnosticStep{
|
||||
{
|
||||
name: "fetch data from Reddit API",
|
||||
fn: func() (string, error) {
|
||||
return testHttpRequest("GET", "https://www.reddit.com/search.json", 200)
|
||||
return testHttpRequestWithHeaders("GET", "https://www.reddit.com/search.json", map[string]string{
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:137.0) Gecko/20100101 Firefox/137.0",
|
||||
}, 200)
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -165,7 +167,7 @@ func testHttpRequestWithHeaders(method, url string, headers map[string]string, e
|
||||
request.Header.Add(key, value)
|
||||
}
|
||||
|
||||
response, err := http.DefaultClient.Do(request)
|
||||
response, err := defaultHTTPClient.Do(request)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
+45
-32
@@ -101,35 +101,37 @@ func newApplication(c *config) (*application, error) {
|
||||
// Init themes
|
||||
//
|
||||
|
||||
themeKeys := make([]string, 0, 2)
|
||||
themeProps := make([]*themeProperties, 0, 2)
|
||||
if !config.Theme.DisablePicker {
|
||||
themeKeys := make([]string, 0, 2)
|
||||
themeProps := make([]*themeProperties, 0, 2)
|
||||
|
||||
defaultDarkTheme, ok := config.Theme.Presets.Get("default-dark")
|
||||
if ok && !config.Theme.SameAs(defaultDarkTheme) || !config.Theme.SameAs(&themeProperties{}) {
|
||||
themeKeys = append(themeKeys, "default-dark")
|
||||
themeProps = append(themeProps, &themeProperties{})
|
||||
}
|
||||
defaultDarkTheme, ok := config.Theme.Presets.Get("default-dark")
|
||||
if ok && !config.Theme.SameAs(defaultDarkTheme) || !config.Theme.SameAs(&themeProperties{}) {
|
||||
themeKeys = append(themeKeys, "default-dark")
|
||||
themeProps = append(themeProps, &themeProperties{})
|
||||
}
|
||||
|
||||
themeKeys = append(themeKeys, "default-light")
|
||||
themeProps = append(themeProps, &themeProperties{
|
||||
Light: true,
|
||||
BackgroundColor: &hslColorField{240, 13, 95},
|
||||
PrimaryColor: &hslColorField{230, 100, 30},
|
||||
NegativeColor: &hslColorField{0, 70, 50},
|
||||
ContrastMultiplier: 1.3,
|
||||
TextSaturationMultiplier: 0.5,
|
||||
})
|
||||
themeKeys = append(themeKeys, "default-light")
|
||||
themeProps = append(themeProps, &themeProperties{
|
||||
Light: true,
|
||||
BackgroundColor: &hslColorField{240, 13, 95},
|
||||
PrimaryColor: &hslColorField{230, 100, 30},
|
||||
NegativeColor: &hslColorField{0, 70, 50},
|
||||
ContrastMultiplier: 1.3,
|
||||
TextSaturationMultiplier: 0.5,
|
||||
})
|
||||
|
||||
themePresets, err := newOrderedYAMLMap(themeKeys, themeProps)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating theme presets: %v", err)
|
||||
}
|
||||
config.Theme.Presets = *themePresets.Merge(&config.Theme.Presets)
|
||||
themePresets, err := newOrderedYAMLMap(themeKeys, themeProps)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating theme presets: %v", err)
|
||||
}
|
||||
config.Theme.Presets = *themePresets.Merge(&config.Theme.Presets)
|
||||
|
||||
for key, properties := range config.Theme.Presets.Items() {
|
||||
properties.Key = key
|
||||
if err := properties.init(); err != nil {
|
||||
return nil, fmt.Errorf("initializing preset theme %s: %v", key, err)
|
||||
for key, properties := range config.Theme.Presets.Items() {
|
||||
properties.Key = key
|
||||
if err := properties.init(); err != nil {
|
||||
return nil, fmt.Errorf("initializing preset theme %s: %v", key, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,6 +172,12 @@ func newApplication(c *config) (*application, error) {
|
||||
page.DesktopNavigationWidth = page.Width
|
||||
}
|
||||
|
||||
for i := range page.HeadWidgets {
|
||||
widget := page.HeadWidgets[i]
|
||||
app.widgetByID[widget.GetID()] = widget
|
||||
widget.setProviders(providers)
|
||||
}
|
||||
|
||||
for c := range page.Columns {
|
||||
column := &page.Columns[c]
|
||||
|
||||
@@ -180,7 +188,6 @@ func newApplication(c *config) (*application, error) {
|
||||
for w := range column.Widgets {
|
||||
widget := column.Widgets[w]
|
||||
app.widgetByID[widget.GetID()] = widget
|
||||
|
||||
widget.setProviders(providers)
|
||||
}
|
||||
}
|
||||
@@ -283,11 +290,13 @@ type templateData struct {
|
||||
func (a *application) populateTemplateRequestData(data *templateRequestData, r *http.Request) {
|
||||
theme := &a.Config.Theme.themeProperties
|
||||
|
||||
selectedTheme, err := r.Cookie("theme")
|
||||
if err == nil {
|
||||
preset, exists := a.Config.Theme.Presets.Get(selectedTheme.Value)
|
||||
if exists {
|
||||
theme = preset
|
||||
if !a.Config.Theme.DisablePicker {
|
||||
selectedTheme, err := r.Cookie("theme")
|
||||
if err == nil {
|
||||
preset, exists := a.Config.Theme.Presets.Get(selectedTheme.Value)
|
||||
if exists {
|
||||
theme = preset
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -431,7 +440,11 @@ func (a *application) server() (func() error, func() error) {
|
||||
mux.HandleFunc("GET /{page}", a.handlePageRequest)
|
||||
|
||||
mux.HandleFunc("GET /api/pages/{page}/content/{$}", a.handlePageContentRequest)
|
||||
mux.HandleFunc("POST /api/set-theme/{key}", a.handleThemeChangeRequest)
|
||||
|
||||
if !a.Config.Theme.DisablePicker {
|
||||
mux.HandleFunc("POST /api/set-theme/{key}", a.handleThemeChangeRequest)
|
||||
}
|
||||
|
||||
mux.HandleFunc("/api/widgets/{widget}/{path...}", a.handleWidgetRequest)
|
||||
mux.HandleFunc("GET /api/healthz", func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
@@ -661,7 +661,7 @@ function setupTruncatedElementTitles() {
|
||||
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
const element = elements[i];
|
||||
if (element.getAttribute("title") === null) element.title = element.textContent;
|
||||
if (element.getAttribute("title") === null) element.title = element.innerText;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -683,17 +683,21 @@ async function changeTheme(key, onChanged) {
|
||||
.appendTo(document.head);
|
||||
|
||||
themeStyleElem.html(newThemeStyle);
|
||||
document.documentElement.setAttribute("data-theme", key);
|
||||
document.documentElement.setAttribute("data-scheme", response.headers.get("X-Scheme"));
|
||||
typeof onChanged == "function" && onChanged();
|
||||
setTimeout(() => { tempStyle.remove(); }, 10);
|
||||
}
|
||||
|
||||
function initThemeSwitcher() {
|
||||
function initThemePicker() {
|
||||
const themeChoicesInMobileNav = find(".mobile-navigation .theme-choices");
|
||||
if (!themeChoicesInMobileNav) return;
|
||||
|
||||
const themeChoicesInHeader = find(".header-container .theme-choices");
|
||||
|
||||
if (themeChoicesInHeader) {
|
||||
themeChoicesInHeader.replaceWith(
|
||||
find(".mobile-navigation .theme-choices").cloneNode(true)
|
||||
themeChoicesInMobileNav.cloneNode(true)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -738,7 +742,7 @@ function initThemeSwitcher() {
|
||||
}
|
||||
|
||||
async function setupPage() {
|
||||
initThemeSwitcher();
|
||||
initThemePicker();
|
||||
|
||||
const pageElement = document.getElementById("page");
|
||||
const pageContentElement = document.getElementById("page-content");
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"html/template"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
"golang.org/x/text/message"
|
||||
@@ -22,6 +21,9 @@ var globalTemplateFunctions = template.FuncMap{
|
||||
"safeURL": func(str string) template.URL {
|
||||
return template.URL(str)
|
||||
},
|
||||
"safeHTML": func(str string) template.HTML {
|
||||
return template.HTML(str)
|
||||
},
|
||||
"absInt": func(i int) int {
|
||||
return int(math.Abs(float64(i)))
|
||||
},
|
||||
@@ -54,7 +56,6 @@ var globalTemplateFunctions = template.FuncMap{
|
||||
|
||||
return template.HTML(value + ` <span class="color-base size-h5">` + label + `</span>`)
|
||||
},
|
||||
"hasPrefix": strings.HasPrefix,
|
||||
}
|
||||
|
||||
func mustParseTemplate(primary string, dependencies ...string) *template.Template {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" id="top" data-scheme="{{ if .Request.Theme.Light }}light{{ else }}dark{{ end }}">
|
||||
<html lang="en" id="top" data-theme="{{ .Request.Theme.Key }}" data-scheme="{{ if .Request.Theme.Light }}light{{ else }}dark{{ end }}">
|
||||
<head>
|
||||
{{ block "document-head-before" . }}{{ end }}
|
||||
<script>
|
||||
@@ -21,7 +21,6 @@
|
||||
<meta name="theme-color" content="{{ .Request.Theme.BackgroundColorAsHex }}">
|
||||
<link rel="apple-touch-icon" sizes="512x512" href='{{ .App.Config.Branding.AppIconURL }}'>
|
||||
<link rel="manifest" href='{{ .App.VersionedAssetPath "manifest.json" }}'>
|
||||
<link rel="icon" type="image/png" href='{{ .App.StaticAssetPath "favicon.png" }}' />
|
||||
<link rel="icon" type="{{ .App.Config.Branding.FaviconType }}" href="{{ .App.Config.Branding.FaviconURL }}" />
|
||||
<link rel="stylesheet" href='{{ .App.StaticAssetPath "css/bundle.css" }}'>
|
||||
<style id="theme-style">{{ .Request.Theme.CSS }}</style>
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
<nav class="nav flex grow hide-scrollbars">
|
||||
{{ template "navigation-links" . }}
|
||||
</nav>
|
||||
{{ if not .App.Config.Theme.DisablePicker }}
|
||||
<div class="theme-picker self-center" data-popover-type="html" data-popover-position="below" data-popover-show-delay="0">
|
||||
<div class="current-theme-preview">
|
||||
{{ .Request.Theme.PreviewHTML }}
|
||||
@@ -41,6 +42,7 @@
|
||||
<div class="theme-choices"></div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{- if .App.RequiresAuth }}
|
||||
<a class="block self-center" href="{{ .App.Config.Server.BaseURL }}/logout" title="Logout">
|
||||
<svg class="logout-button" stroke="var(--color-text-subdue)" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5">
|
||||
@@ -66,6 +68,7 @@
|
||||
</div>
|
||||
|
||||
<div class="mobile-navigation-actions flex flex-column margin-block-10">
|
||||
{{ if not .App.Config.Theme.DisablePicker }}
|
||||
<div class="theme-picker flex justify-between items-center" data-popover-type="html" data-popover-position="above" data-popover-show-delay="0" data-popover-hide-delay="100" data-popover-anchor=".current-theme-preview" data-popover-trigger="click">
|
||||
<div data-popover-html>
|
||||
<div class="theme-choices">
|
||||
@@ -87,6 +90,7 @@
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{ if .App.RequiresAuth }}
|
||||
<a href="{{ .App.Config.Server.BaseURL }}/logout" class="flex justify-between items-center">
|
||||
@@ -100,7 +104,7 @@
|
||||
</div>
|
||||
|
||||
<div class="content-bounds grow{{ if .Page.Width }} content-bounds-{{ .Page.Width }}{{ end }}">
|
||||
<main class="page{{ if .Page.CenterVertically }} page-center-vertically{{ end }}" id="page" aria-live="polite" aria-busy="true">
|
||||
<main class="page{{ if .Page.CenterVertically }} center-vertically{{ end }}" id="page" aria-live="polite" aria-busy="true">
|
||||
<h1 class="visually-hidden">{{ .Page.Title }}</h1>
|
||||
<div class="page-content" id="page-content"></div>
|
||||
<div class="page-loading-container">
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<li class="flex thumbnail-parent gap-10 items-center">
|
||||
<img class="video-horizontal-list-thumbnail thumbnail" loading="lazy" src="{{ .ThumbnailUrl }}" alt="">
|
||||
<div class="min-width-0">
|
||||
<a class="block text-truncate color-primary-if-not-visited" href="{{ .Url }}" target="_blank" rel="noreferrer">{{ .Title }}</a>
|
||||
<a class="block text-truncate color-primary-if-not-visited" href="{{ .Url | safeURL }}" target="_blank" rel="noreferrer">{{ .Title }}</a>
|
||||
<ul class="list-horizontal-text flex-nowrap">
|
||||
<li class="shrink-0" {{ dynamicRelativeTimeAttrs .TimePosted }}></li>
|
||||
<li class="min-width-0">
|
||||
|
||||
@@ -108,6 +108,20 @@ func (o *customAPIOptions) BoolOr(key string, defaultValue bool) bool {
|
||||
return customAPIGetOptionOrDefault(*o, key, defaultValue)
|
||||
}
|
||||
|
||||
func (o *customAPIOptions) JSON(key string) string {
|
||||
value, exists := (*o)[key]
|
||||
if !exists {
|
||||
panic(fmt.Sprintf("key %q does not exist in options", key))
|
||||
}
|
||||
|
||||
encoded, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("marshaling %s: %v", key, err))
|
||||
}
|
||||
|
||||
return string(encoded)
|
||||
}
|
||||
|
||||
func customAPIGetOptionOrDefault[T any](o customAPIOptions, key string, defaultValue T) T {
|
||||
if value, exists := o[key]; exists {
|
||||
if typedValue, ok := value.(T); ok {
|
||||
@@ -479,6 +493,12 @@ var customAPITemplateFuncs = func() template.FuncMap {
|
||||
"div": func(a, b any) any {
|
||||
return doMathOpWithAny(a, b, "div")
|
||||
},
|
||||
"mod": func(a, b int) int {
|
||||
if b == 0 {
|
||||
return 0
|
||||
}
|
||||
return a % b
|
||||
},
|
||||
"now": func() time.Time {
|
||||
return time.Now()
|
||||
},
|
||||
@@ -509,6 +529,12 @@ var customAPITemplateFuncs = func() template.FuncMap {
|
||||
// Shorthand to do both of the above with a single function call
|
||||
return dynamicRelativeTimeAttrs(customAPIFuncParseTimeInLocation(layout, value, time.UTC))
|
||||
},
|
||||
"startOfDay": func(t time.Time) time.Time {
|
||||
return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
|
||||
},
|
||||
"endOfDay": func(t time.Time) time.Time {
|
||||
return time.Date(t.Year(), t.Month(), t.Day(), 23, 59, 59, 0, t.Location())
|
||||
},
|
||||
// The reason we flip the parameter order is so that you can chain multiple calls together like this:
|
||||
// {{ .JSON.String "foo" | trimPrefix "bar" | doSomethingElse }}
|
||||
// instead of doing this:
|
||||
|
||||
@@ -26,6 +26,7 @@ const defaultClientTimeout = 5 * time.Second
|
||||
var defaultHTTPClient = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
MaxIdleConnsPerHost: 10,
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
},
|
||||
Timeout: defaultClientTimeout,
|
||||
}
|
||||
@@ -34,6 +35,7 @@ var defaultInsecureHTTPClient = &http.Client{
|
||||
Timeout: defaultClientTimeout,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user