Browse Source

Add more features to clock

Svilen Markov 1 năm trước cách đây
mục cha
commit
8148f09b9c

+ 33 - 1
docs/configuration.md

@@ -966,18 +966,50 @@ Whether to open the link in the same tab or a new one.
 Whether to hide the colored arrow on each link.
 
 ### Clock
-Display a clock showing the current time.
+Display a clock showing the current time and date. Optionally, also display the the time in other timezones.
 
 Example:
 
 ```yaml
 - type: clock
+  hour-format: 24h
+  timezones:
+    - timezone: Europe/Paris
+      label: Paris
+    - timezone: America/New_York
+      label: New York
+    - timezone: Asia/Tokyo
+      label: Tokyo
 ```
 
 Preview:
 
 ![](images/clock-widget-preview.png)
 
+#### Properties
+
+| Name | Type | Required | Default |
+| ---- | ---- | -------- | ------- |
+| hour-format | string | no | 24h |
+| timezones | array | no |  |
+
+##### `hour-format`
+Whether to show the time in 12 or 24 hour format. Possible values are `12h` and `24h`.
+
+#### Properties for each timezone
+
+| Name | Type | Required | Default |
+| ---- | ---- | -------- | ------- |
+| timezone | string | yes | |
+| label | string | no | |
+
+##### `timezone`
+A timezone identifier such as `Europe/London`, `America/New_York`, etc. The full list of available identifiers can be found [here](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones).
+
+##### `label`
+Optionally, override the display value for the timezone to something more meaningful such as "Home", "Work" or anything else.
+
+
 ### Calendar
 Display a calendar.
 

BIN
docs/images/clock-widget-preview.png


+ 4 - 0
internal/assets/static/main.css

@@ -849,6 +849,10 @@ body {
     transform: translate(-50%, -50%);
 }
 
+.clock-time span {
+    color: var(--color-text-highlight);
+}
+
 .monitor-site-icon {
     display: block;
     opacity: 0.8;

+ 101 - 9
internal/assets/static/main.js

@@ -103,7 +103,7 @@ function updateRelativeTimeForElements(elements)
         if (timestamp === undefined)
             continue
 
-        element.innerText = relativeTimeSince(timestamp);
+        element.textContent = relativeTimeSince(timestamp);
     }
 }
 
@@ -341,18 +341,110 @@ function afterContentReady(callback) {
     contentReadyCallbacks.push(callback);
 }
 
-function updateClocks(elements, formatter) {
-    const currentDate = new Date();
-    for (const elem of elements) {
-        elem.textContent = formatter.format(currentDate);
+const weekDayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
+const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
+
+function makeSettableTimeElement(element, hourFormat) {
+    const fragment = document.createDocumentFragment();
+    const hour = document.createElement('span');
+    const minute = document.createElement('span');
+    const amPm = document.createElement('span');
+    fragment.append(hour, document.createTextNode(':'), minute);
+
+    if (hourFormat == '12h') {
+        fragment.append(document.createTextNode(' '), amPm);
     }
+
+    element.append(fragment);
+
+    return (date) => {
+        const hours = date.getHours();
+
+        if (hourFormat == '12h') {
+            amPm.textContent = hours < 12 ? 'AM' : 'PM';
+            hour.textContent = hours % 12 || 12;
+        } else {
+            hour.textContent = hours < 10 ? '0' + hours : hours;
+        }
+
+        const minutes = date.getMinutes();
+        minute.textContent = minutes < 10 ? '0' + minutes : minutes;
+    };
+};
+
+function timeInZone(now, zone) {
+    let timeInZone;
+
+    try {
+        timeInZone = new Date(now.toLocaleString('en-US', { timeZone: zone }));
+    } catch (e) {
+        // TODO: indicate to the user that this is an invalid timezone
+        console.error(e);
+        timeInZone = now
+    }
+
+    const diffInHours = Math.round((timeInZone.getTime() - now.getTime()) / 1000 / 60 / 60);
+
+    return { time: timeInZone, diffInHours: diffInHours };
 }
 
 function setupClocks() {
-    const clockFormatter = new Intl.DateTimeFormat(undefined, {minute: "numeric", hour: "numeric"});
-    const elements = document.getElementsByClassName("glance-clock");
-    updateClocks(elements, clockFormatter)
-    setInterval(() => {updateClocks(elements, clockFormatter)}, 1000);
+    const clocks = document.getElementsByClassName('clock');
+
+    if (clocks.length == 0) {
+        return;
+    }
+
+    const updateCallbacks = [];
+
+    for (var i = 0; i < clocks.length; i++) {
+        const clock = clocks[i];
+        const hourFormat = clock.dataset.hourFormat;
+        const localTimeContainer = clock.querySelector('[data-local-time]');
+        const localDateElement = localTimeContainer.querySelector('[data-date]');
+        const localWeekdayElement = localTimeContainer.querySelector('[data-weekday]');
+        const localYearElement = localTimeContainer.querySelector('[data-year]');
+        const timeZoneContainers = clock.querySelectorAll('[data-time-in-zone]');
+
+        const setLocalTime = makeSettableTimeElement(
+            localTimeContainer.querySelector('[data-time]'),
+            hourFormat
+        );
+
+        updateCallbacks.push((now) => {
+            setLocalTime(now);
+            localDateElement.textContent = now.getDate() + ' ' + monthNames[now.getMonth()];
+            localWeekdayElement.textContent = weekDayNames[now.getDay()];
+            localYearElement.textContent = now.getFullYear();
+        });
+
+        for (var z = 0; z < timeZoneContainers.length; z++) {
+            const timeZoneContainer = timeZoneContainers[z];
+            const diffElement = timeZoneContainer.querySelector('[data-time-diff]');
+
+            const setZoneTime = makeSettableTimeElement(
+                timeZoneContainer.querySelector('[data-time]'),
+                hourFormat
+            );
+
+            updateCallbacks.push((now) => {
+                const { time, diffInHours } = timeInZone(now, timeZoneContainer.dataset.timeInZone);
+                setZoneTime(time);
+                diffElement.textContent = (diffInHours <= 0 ? diffInHours : '+' + diffInHours) + 'h';
+            });
+        }
+    }
+
+    const updateClocks = () => {
+        const now = new Date();
+
+        for (var i = 0; i < updateCallbacks.length; i++)
+            updateCallbacks[i](now);
+
+        setTimeout(updateClocks, (60 - now.getSeconds()) * 1000);
+    };
+
+    updateClocks();
 }
 
 async function setupPage() {

+ 26 - 1
internal/assets/templates/clock.html

@@ -1,5 +1,30 @@
 {{ template "widget-base.html" . }}
 
 {{ define "widget-content" }}
-<div class="size-h2 color-highlight text-center glance-clock"></div>
+<div class="clock" data-hour-format="{{ .HourFormat }}">
+    <div class="flex justify-between items-center" data-local-time>
+        <div>
+            <div class="color-highlight size-h1" data-date></div>
+            <div data-year></div>
+        </div>
+        <div class="text-right">
+            <div class="clock-time size-h1" data-time></div>
+            <div data-weekday></div>
+        </div>
+    </div>
+    {{ if gt (len .Timezones) 0 }}
+    <hr class="margin-block-10">
+    <ul class="list list-gap-10">
+        {{ range .Timezones }}
+        <li class="flex items-center gap-15" data-time-in-zone="{{ .Timezone }}">
+            <div class="grow min-width-0">
+                <div class="text-truncate">{{ if ne .Label "" }}{{ .Label }}{{ else }}{{ .Timezone }}{{ end }}</div>
+            </div>
+            <div class="color-subdue" data-time-diff></div>
+            <div class="size-h4 clock-time shrink-0 text-right" data-time></div>
+        </li>
+        {{ end }}
+    </ul>
+    {{ end }}
+</div>
 {{ end }}

+ 31 - 4
internal/widget/clock.go

@@ -1,23 +1,50 @@
 package widget
 
 import (
-	"context"
+	"errors"
+	"fmt"
 	"html/template"
+	"time"
 
 	"github.com/glanceapp/glance/internal/assets"
 )
 
 type Clock struct {
 	widgetBase `yaml:",inline"`
+	cachedHTML template.HTML `yaml:"-"`
+	HourFormat string        `yaml:"hour-format"`
+	Timezones  []struct {
+		Timezone string `yaml:"timezone"`
+		Label    string `yaml:"label"`
+	} `yaml:"timezones"`
 }
 
 func (widget *Clock) Initialize() error {
 	widget.withTitle("Clock").withError(nil)
+
+	if widget.HourFormat == "" {
+		widget.HourFormat = "24h"
+	} else if widget.HourFormat != "12h" && widget.HourFormat != "24h" {
+		return errors.New("invalid hour format for clock widget, must be either 12h or 24h")
+	}
+
+	for t := range widget.Timezones {
+		if widget.Timezones[t].Timezone == "" {
+			return errors.New("missing timezone value for clock widget")
+		}
+
+		_, err := time.LoadLocation(widget.Timezones[t].Timezone)
+
+		if err != nil {
+			return fmt.Errorf("invalid timezone '%s' for clock widget: %v", widget.Timezones[t].Timezone, err)
+		}
+	}
+
+	widget.cachedHTML = widget.render(widget, assets.ClockTemplate)
+
 	return nil
 }
 
-func (widget *Clock) Update(ctx context.Context) {}
-
 func (widget *Clock) Render() template.HTML {
-	return widget.render(widget, assets.ClockTemplate)
+	return widget.cachedHTML
 }