فهرست منبع

Add widget: Count Timer

MetricVoid 3 ماه پیش
والد
کامیت
791ec78139

+ 42 - 0
docs/configuration.md

@@ -39,6 +39,7 @@
   - [Twitch Top Games](#twitch-top-games)
   - [Twitch Top Games](#twitch-top-games)
   - [iframe](#iframe)
   - [iframe](#iframe)
   - [HTML](#html)
   - [HTML](#html)
+  - [Count Timer](#count-timer)
 
 
 
 
 ## Preconfigured page
 ## Preconfigured page
@@ -2458,3 +2459,44 @@ Example:
 ```
 ```
 
 
 Note the use of `|` after `source:`, this allows you to insert a multi-line string.
 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


+ 95 - 1
internal/glance/static/js/main.js

@@ -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() {
 async function setupPage() {
     const pageElement = document.getElementById("page");
     const pageElement = document.getElementById("page");
     const pageContentElement = document.getElementById("page-content");
     const pageContentElement = document.getElementById("page-content");
@@ -662,7 +755,8 @@ async function setupPage() {
 
 
     try {
     try {
         setupPopovers();
         setupPopovers();
-        setupClocks()
+        setupClocks();
+        setUpCountdowns();
         await setupCalendars();
         await setupCalendars();
         setupCarousels();
         setupCarousels();
         setupSearchBoxes();
         setupSearchBoxes();

+ 33 - 0
internal/glance/templates/count-timer.html

@@ -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 }}

+ 56 - 0
internal/glance/widget-count-timer.go

@@ -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
+}

+ 2 - 0
internal/glance/widget.go

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