Add widget: Count Timer

This commit is contained in:
MetricVoid 2025-03-14 23:18:58 -04:00
parent d22ac6a7a4
commit 791ec78139
6 changed files with 228 additions and 1 deletions

View file

@ -39,6 +39,7 @@
- [Twitch Top Games](#twitch-top-games)
- [iframe](#iframe)
- [HTML](#html)
- [Count Timer](#count-timer)
## Preconfigured page
@ -2458,3 +2459,44 @@ Example:
```
Note the use of `|` after `source:`, this allows you to insert a multi-line string.
### Count Timer
Adds a counting timer. Counts up or down from/to a specific time. This widget is dynamic and updates every second.
Example:
```yaml
- type: count-timer
event-title: Christmas 2025
date: 2025-12-25T00:00:00Z
- type: count-timer
event-title: New Year 2022 (US EST)
date: 2022-01-01T00:00:00-05:00
- type: count-timer
title: Conf 2022
date: 2022-06-04T00:00:00Z
href: https://conf.com
```
Preview:
![](images/count-timer.png)
#### Properties
| Name | Type | Required | Default | Description |
| ---- | ---- | -------- | ------- | ----------- |
| date | time | yes | | Time, including timezone info |
| title | str | no | | Choose one between `titie` end `event-title` |
| event-title | str | no | | Choose one between `titie` end `event-title` |
| href | str(URL) | no | | |
#### `date`
The target date to count to. ISO 8601 format
#### `event-title`
The event title. ` ⋅ PAST` or ` ⋅ FUTURE` will be added to form the widget title.
#### `title`
If `event-title` is not set or empty, this title shows instead.
#### `href`
If set, the counter will have a hyperlink to this URL.

BIN
docs/images/count-timer.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View file

@ -653,6 +653,99 @@ function setupTruncatedElementTitles() {
}
}
function setUpCountdowns() {
const countdowns = document.getElementsByClassName("widget-type-count-timer");
if (countdowns.length == 0) {
return;
}
const updateCallbacks = [];
for (var i = 0; i < countdowns.length; i++) {
const countdown = countdowns[i];
const datasetMeta = countdown.getElementsByTagName("meta")[0];
const targetDate = new Date(datasetMeta.getAttribute("target"));
const title = datasetMeta.getAttribute("title");
const eventTitle = datasetMeta.getAttribute("event");
const h2Element = countdown.getElementsByTagName("h2")[0];
const dayTd = countdown.getElementsByClassName("count-days")[0];
const hourTd = countdown.getElementsByClassName("count-hours")[0];
const minuteTd = countdown.getElementsByClassName("count-minutes")[0];
const secondTd = countdown.getElementsByClassName("count-seconds")[0];
// Create update callback fn
const updateCountdown = (now) => {
// Timezone
var diff = targetDate - now;
// Remove all colors
dayTd.classList.remove("color-primary")
dayTd.classList.remove("color-highlight")
hourTd.classList.remove("color-primary")
hourTd.classList.remove("color-highlight")
minuteTd.classList.remove("color-primary")
minuteTd.classList.remove("color-highlight")
secondTd.classList.remove("color-primary")
secondTd.classList.remove("color-highlight")
if(diff > 0) {
// Set color to primary
dayTd.classList.add("color-primary")
hourTd.classList.add("color-primary")
minuteTd.classList.add("color-primary")
secondTd.classList.add("color-primary")
} else {
// Set color to highlight
dayTd.classList.add("color-highlight")
hourTd.classList.add("color-highlight")
minuteTd.classList.add("color-highlight")
secondTd.classList.add("color-highlight")
}
if(eventTitle & (eventTitle != "")) {
if(diff > 0) {
h2Element.textContent = eventTitle + " ⋅ FUTURE"
} else {
h2Element.textContent = eventTitle + " ⋅ PAST"
}
} else {
h2Element.textContent = title;
}
if(diff < 0) {
diff = -diff;
}
const diffDays = Math.floor(diff / (1000 * 60 * 60 * 24));
const diffHours = String(Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60))).padStart(2, '0');
const diffMinutes = String(Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60))).padStart(2, '0');
const diffSeconds = String(Math.floor((diff % (1000 * 60)) / 1000)).padStart(2, '0');
// Set up countdown
dayTd.innerHTML = diffDays;
hourTd.innerHTML = diffHours;
minuteTd.innerHTML = diffMinutes;
secondTd.innerHTML = diffSeconds;
}
updateCallbacks.push(updateCountdown);
}
const updateCountdowns = () => {
const now = new Date();
for (var i = 0; i < updateCallbacks.length; i++)
updateCallbacks[i](now);
setTimeout(updateCountdowns, 1000);
}
updateCountdowns();
}
async function setupPage() {
const pageElement = document.getElementById("page");
const pageContentElement = document.getElementById("page-content");
@ -662,7 +755,8 @@ async function setupPage() {
try {
setupPopovers();
setupClocks()
setupClocks();
setUpCountdowns();
await setupCalendars();
setupCarousels();
setupSearchBoxes();

View file

@ -0,0 +1,33 @@
{{ template "widget-base.html" . }}
{{ define "widget-header" }}
<h2 class="uppercase"> {{ .RenderedTitle }}</h2>
{{ end }}
{{ define "widget-content" }}
{{ if .Href }}
<a href="{{ .Href }}" target="_blank" rel="noreferrer" text-decoration: none; color: inherit;>
{{ end }}
<div class="count-timer=body">
<meta id="dataset" target="{{ .TargetDate }}" event="{{ .EventTitle }}" title="{{.Title}}" />
<table style="width: 100%;">
<tbody>
<tr>
<td class="title size-h1 count-days" style="text-align: center"> {{ .Days }} </td>
<td class="title size-h1 count-hours" style="text-align: center"> {{ .Hours }} </td>
<td class="title size-h1 count-minutes" style="text-align: center"> {{ .Minutes }} </td>
<td class="title size-h1 count-seconds" style="text-align: center"> {{ .Seconds }} </td>
</tr>
<tr>
<td class="color-secondary" style="text-align: center">DAYS</td>
<td class="color-secondary" style="text-align: center">HRS</td>
<td class="color-secondary" style="text-align: center">MIN</td>
<td class="color-secondary" style="text-align: center">SEC</td>
</tr>
</tbody>
</table>
</div>
{{ if .Href }}
</a>
{{ end }}
{{ end }}

View file

@ -0,0 +1,56 @@
package glance
import (
"context"
"html/template"
"time"
)
var countTimerWidgetTemplate = mustParseTemplate("count-timer.html", "widget-base.html")
type countTimerWidget struct {
widgetBase `yaml:",inline"`
cachedHTML template.HTML `yaml:"-"`
EventTitle string `yaml:"event-title"`
TargetDate time.Time `yaml:"date"`
Href string `yaml:"href"`
RenderedTitle string `yaml:"-"`
DiffSeconds int `yaml:"-"`
Days int `yaml:"-"`
Hours int `yaml:"-"`
Minutes int `yaml:"-"`
Seconds int `yaml:"-"`
}
func (w *countTimerWidget) update(ctx context.Context) {
now := time.Now()
target := w.TargetDate
diff := target.Sub(now)
if diff < 0 {
w.RenderedTitle = w.EventTitle + " ⋅ PAST"
diff = -diff
} else {
w.RenderedTitle = w.EventTitle + " ⋅ FUTURE"
}
if w.EventTitle == "" {
w.RenderedTitle = w.Title
}
w.Days = int(diff.Hours()) / 24
w.Hours = int(diff.Hours()) % 24
w.Minutes = int(diff.Minutes()) % 60
w.Seconds = int(diff.Seconds()) % 60
w.cachedHTML = w.renderTemplate(w, countTimerWidgetTemplate)
}
func (w *countTimerWidget) initialize() error {
w.update(context.Background())
w.withTitle(w.RenderedTitle).withError(nil)
return nil
}
func (w *countTimerWidget) Render() template.HTML {
w.update(context.TODO())
return w.cachedHTML
}

View file

@ -79,6 +79,8 @@ func newWidget(widgetType string) (widget, error) {
w = &dockerContainersWidget{}
case "server-stats":
w = &serverStatsWidget{}
case "count-timer":
w = &countTimerWidget{}
default:
return nil, fmt.Errorf("unknown widget type: %s", widgetType)
}