Merge pull request #82 from yardenshoham/add-clock-widget

Add a clock widget
This commit is contained in:
Svilen Markov 2024-05-25 02:32:18 +01:00 committed by GitHub
commit 0f42567d98
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 243 additions and 1 deletions

View File

@ -11,6 +11,7 @@
* Weather * Weather
* Bookmarks * Bookmarks
* Latest YouTube videos from specific channels * Latest YouTube videos from specific channels
* Clock
* Calendar * Calendar
* Stocks * Stocks
* iframe * iframe

View File

@ -17,6 +17,7 @@
- [Repository](#repository) - [Repository](#repository)
- [Bookmarks](#bookmarks) - [Bookmarks](#bookmarks)
- [Calendar](#calendar) - [Calendar](#calendar)
- [Clock](#clock)
- [Stocks](#stocks) - [Stocks](#stocks)
- [Twitch Channels](#twitch-channels) - [Twitch Channels](#twitch-channels)
- [Twitch Top Games](#twitch-top-games) - [Twitch Top Games](#twitch-top-games)
@ -34,6 +35,7 @@ pages:
columns: columns:
- size: small - size: small
widgets: widgets:
- type: clock
- type: calendar - type: calendar
- type: rss - type: rss
@ -963,6 +965,51 @@ Whether to open the link in the same tab or a new one.
Whether to hide the colored arrow on each link. Whether to hide the colored arrow on each link.
### Clock
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 ### Calendar
Display a calendar. Display a calendar.

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

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

View File

@ -103,7 +103,7 @@ function updateRelativeTimeForElements(elements)
if (timestamp === undefined) if (timestamp === undefined)
continue continue
element.innerText = relativeTimeSince(timestamp); element.textContent = relativeTimeSince(timestamp);
} }
} }
@ -341,6 +341,112 @@ function afterContentReady(callback) {
contentReadyCallbacks.push(callback); contentReadyCallbacks.push(callback);
} }
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 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() { 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");
@ -349,6 +455,7 @@ async function setupPage() {
pageContentElement.innerHTML = pageContent; pageContentElement.innerHTML = pageContent;
try { try {
setupClocks()
setupCarousels(); setupCarousels();
setupCollapsibleLists(); setupCollapsibleLists();
setupCollapsibleGrids(); setupCollapsibleGrids();

View File

@ -15,6 +15,7 @@ var (
PageTemplate = compileTemplate("page.html", "document.html", "page-style-overrides.gotmpl") PageTemplate = compileTemplate("page.html", "document.html", "page-style-overrides.gotmpl")
PageContentTemplate = compileTemplate("content.html") PageContentTemplate = compileTemplate("content.html")
CalendarTemplate = compileTemplate("calendar.html", "widget-base.html") CalendarTemplate = compileTemplate("calendar.html", "widget-base.html")
ClockTemplate = compileTemplate("clock.html", "widget-base.html")
BookmarksTemplate = compileTemplate("bookmarks.html", "widget-base.html") BookmarksTemplate = compileTemplate("bookmarks.html", "widget-base.html")
IFrameTemplate = compileTemplate("iframe.html", "widget-base.html") IFrameTemplate = compileTemplate("iframe.html", "widget-base.html")
WeatherTemplate = compileTemplate("weather.html", "widget-base.html") WeatherTemplate = compileTemplate("weather.html", "widget-base.html")

View File

@ -0,0 +1,30 @@
{{ template "widget-base.html" . }}
{{ define "widget-content" }}
<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 }}

50
internal/widget/clock.go Normal file
View File

@ -0,0 +1,50 @@
package widget
import (
"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) Render() template.HTML {
return widget.cachedHTML
}

View File

@ -19,6 +19,8 @@ func New(widgetType string) (Widget, error) {
switch widgetType { switch widgetType {
case "calendar": case "calendar":
return &Calendar{}, nil return &Calendar{}, nil
case "clock":
return &Clock{}, nil
case "weather": case "weather":
return &Weather{}, nil return &Weather{}, nil
case "bookmarks": case "bookmarks":