From 4a27ec3271f19dc51f34769cf42a39e540f8b0c9 Mon Sep 17 00:00:00 2001 From: Yarden Shoham Date: Sat, 18 May 2024 22:40:33 +0300 Subject: [PATCH 1/2] Add a clock widget It's a simple clock that shows the current time Signed-off-by: Yarden Shoham --- README.md | 1 + docs/configuration.md | 15 +++++++++++++++ docs/images/clock-widget-preview.png | Bin 0 -> 2812 bytes internal/assets/static/main.js | 15 +++++++++++++++ internal/assets/templates.go | 1 + internal/assets/templates/clock.html | 5 +++++ internal/widget/clock.go | 23 +++++++++++++++++++++++ internal/widget/widget.go | 2 ++ 8 files changed, 62 insertions(+) create mode 100644 docs/images/clock-widget-preview.png create mode 100644 internal/assets/templates/clock.html create mode 100644 internal/widget/clock.go diff --git a/README.md b/README.md index 715c8e5..44e5bc0 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ * Weather * Bookmarks * Latest YouTube videos from specific channels +* Clock * Calendar * Stocks * iframe diff --git a/docs/configuration.md b/docs/configuration.md index a88f811..5e317a8 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -17,6 +17,7 @@ - [Repository](#repository) - [Bookmarks](#bookmarks) - [Calendar](#calendar) + - [Clock](#clock) - [Stocks](#stocks) - [Twitch Channels](#twitch-channels) - [Twitch Top Games](#twitch-top-games) @@ -34,6 +35,7 @@ pages: columns: - size: small widgets: + - type: clock - type: calendar - type: rss @@ -963,6 +965,19 @@ 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. + +Example: + +```yaml +- type: clock +``` + +Preview: + +![](images/clock-widget-preview.png) + ### Calendar Display a calendar. diff --git a/docs/images/clock-widget-preview.png b/docs/images/clock-widget-preview.png new file mode 100644 index 0000000000000000000000000000000000000000..346a6ed5c948979311d7b95d0f09c94ec113d3e7 GIT binary patch literal 2812 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 {updateClocks(elements, clockFormatter)}, 1000); +} + async function setupPage() { const pageElement = document.getElementById("page"); const pageContentElement = document.getElementById("page-content"); @@ -349,6 +363,7 @@ async function setupPage() { pageContentElement.innerHTML = pageContent; try { + setupClocks() setupCarousels(); setupCollapsibleLists(); setupCollapsibleGrids(); diff --git a/internal/assets/templates.go b/internal/assets/templates.go index b8aa6ae..8dff7c0 100644 --- a/internal/assets/templates.go +++ b/internal/assets/templates.go @@ -15,6 +15,7 @@ var ( PageTemplate = compileTemplate("page.html", "document.html", "page-style-overrides.gotmpl") PageContentTemplate = compileTemplate("content.html") CalendarTemplate = compileTemplate("calendar.html", "widget-base.html") + ClockTemplate = compileTemplate("clock.html", "widget-base.html") BookmarksTemplate = compileTemplate("bookmarks.html", "widget-base.html") IFrameTemplate = compileTemplate("iframe.html", "widget-base.html") WeatherTemplate = compileTemplate("weather.html", "widget-base.html") diff --git a/internal/assets/templates/clock.html b/internal/assets/templates/clock.html new file mode 100644 index 0000000..5116782 --- /dev/null +++ b/internal/assets/templates/clock.html @@ -0,0 +1,5 @@ +{{ template "widget-base.html" . }} + +{{ define "widget-content" }} +
+{{ end }} diff --git a/internal/widget/clock.go b/internal/widget/clock.go new file mode 100644 index 0000000..efb2c8e --- /dev/null +++ b/internal/widget/clock.go @@ -0,0 +1,23 @@ +package widget + +import ( + "context" + "html/template" + + "github.com/glanceapp/glance/internal/assets" +) + +type Clock struct { + widgetBase `yaml:",inline"` +} + +func (widget *Clock) Initialize() error { + widget.withTitle("Clock").withError(nil) + return nil +} + +func (widget *Clock) Update(ctx context.Context) {} + +func (widget *Clock) Render() template.HTML { + return widget.render(widget, assets.ClockTemplate) +} diff --git a/internal/widget/widget.go b/internal/widget/widget.go index 3707b7e..934a92e 100644 --- a/internal/widget/widget.go +++ b/internal/widget/widget.go @@ -19,6 +19,8 @@ func New(widgetType string) (Widget, error) { switch widgetType { case "calendar": return &Calendar{}, nil + case "clock": + return &Clock{}, nil case "weather": return &Weather{}, nil case "bookmarks": From 8148f09b9c4ad386d9a78b704d15707c38d0a305 Mon Sep 17 00:00:00 2001 From: Svilen Markov <7613769+svilenmarkov@users.noreply.github.com> Date: Sat, 25 May 2024 02:28:45 +0100 Subject: [PATCH 2/2] Add more features to clock --- docs/configuration.md | 34 ++++++++- docs/images/clock-widget-preview.png | Bin 2812 -> 11130 bytes internal/assets/static/main.css | 4 + internal/assets/static/main.js | 110 ++++++++++++++++++++++++--- internal/assets/templates/clock.html | 27 ++++++- internal/widget/clock.go | 35 ++++++++- 6 files changed, 195 insertions(+), 15 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 5e317a8..44e803f 100644 --- a/docs/configuration.md +++ b/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. diff --git a/docs/images/clock-widget-preview.png b/docs/images/clock-widget-preview.png index 346a6ed5c948979311d7b95d0f09c94ec113d3e7..bf809c5ad187f4fc44065fcbe8a0bd94424fce42 100644 GIT binary patch literal 11130 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 literal 2812 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 { + 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() { diff --git a/internal/assets/templates/clock.html b/internal/assets/templates/clock.html index 5116782..2be2d1c 100644 --- a/internal/assets/templates/clock.html +++ b/internal/assets/templates/clock.html @@ -1,5 +1,30 @@ {{ template "widget-base.html" . }} {{ define "widget-content" }} -
+
+
+
+
+
+
+
+
+
+
+
+ {{ if gt (len .Timezones) 0 }} +
+
    + {{ range .Timezones }} +
  • +
    +
    {{ if ne .Label "" }}{{ .Label }}{{ else }}{{ .Timezone }}{{ end }}
    +
    +
    +
    +
  • + {{ end }} +
+ {{ end }} +
{{ end }} diff --git a/internal/widget/clock.go b/internal/widget/clock.go index efb2c8e..efe8ccd 100644 --- a/internal/widget/clock.go +++ b/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 }