Compare commits

...
34 Commits
Author SHA1 Message Date
Svilen Markov 7743664527 Merge pull request #57 from glanceapp/release/v0.4.0
Release v0.4.0
2024-05-12 22:12:00 +01:00
Svilen Markov 714e1b6d29 Add dynamic columns style for bookmarks, stocks & monitor sites 2024-05-12 22:07:58 +01:00
Svilen Markov a0585e097f Update codeblocks highlighting 2024-05-12 20:45:57 +01:00
Svilen Markov 6363b76d33 Update codeblocks highlighting 2024-05-12 20:44:35 +01:00
Svilen Markov 503562f2e5 Update readme 2024-05-12 20:42:36 +01:00
Svilen Markov 01a7331354 Update readme with docker image build guide 2024-05-12 20:33:36 +01:00
Svilen Markov 6bb44bef20 Merge pull request #54 from eltociear/patch-1
docs: update configuration.md
2024-05-12 11:03:18 +01:00
Svilen Markov aef0c84286 Move engagement calculation inside widget 2024-05-12 11:00:45 +01:00
Svilen Markov 0fbc8dcb41 Allow specifying links for stock symbol & chart 2024-05-12 04:13:38 +01:00
Svilen Markov 818138340c Rename widget to just repository 2024-05-12 03:35:25 +01:00
Svilen Markov 4beab6bcec Make images fade in as they load 2024-05-12 03:18:55 +01:00
Ikko Eltociear Ashimine b9b7ae1cac docs: update configuration.md
minor fix
2024-05-12 03:09:12 +09:00
Svilen Markov 82f51293df Add more refinement options for HN widget 2024-05-10 14:08:01 +01:00
Svilen Markov 840ba0f240 Add more refinement options for Reddit widget 2024-05-10 13:23:57 +01:00
Svilen Markov c5e3eed64b Add Repository Overview widget 2024-05-10 11:05:42 +01:00
Svilen Markov 7dd1cca65d Add horizontal-cards-2 style for RSS widget 2024-05-10 10:10:13 +01:00
Svilen Markov 57414bdb67 So. Many. Units. 2024-05-10 09:52:05 +01:00
Svilen Markov 646be63895 Fix 1px black border in video thumbnails 2024-05-10 08:26:55 +01:00
Svilen Markov 3c1fb97c12 Add grid-cards style for videos widget 2024-05-10 07:41:11 +01:00
Svilen Markov e78619f5e1 Update build to include openbsd and armv7 2024-05-09 13:00:42 +01:00
Svilen Markov 54102ae35e Make RSS thumbnail height configurable 2024-05-09 06:01:16 +01:00
Svilen Markov 7c4c1798b6 Lowercase domains 2024-05-09 04:49:42 +01:00
Svilen Markov f30006093d Tweak breakpoints of horizontal cards 2024-05-09 04:46:22 +01:00
Svilen Markov c8ef47064a Merge branch 'main' into release/v0.4.0 2024-05-09 04:22:51 +01:00
Svilen Markov 56cb97706b Document show-mobile-header property 2024-05-08 01:38:49 +01:00
Svilen Markov dd5bec799f Allow specifying Url from env 2024-05-08 01:30:02 +01:00
Svilen Markov 2389415071 Add mobile reachability header 2024-05-08 01:21:55 +01:00
Svilen Markov 976fb02bd3 Lower gap between releases 2024-05-06 16:53:24 +01:00
Svilen Markov 007fe18d27 Add utility classes 2024-05-06 16:53:13 +01:00
Svilen Markov 11f2999b52 Update readme 2024-05-06 16:52:56 +01:00
Svilen Markov e204029d3c Merge pull request #41 from chev2/catppuccin-themes
Add the other Catppuccin themes
2024-05-03 22:09:39 +01:00
Chev d3eaae2fb0 Update docs/themes.md with new Catppuccin themes 2024-05-03 12:52:02 -07:00
Chev 251c22a14f Rename catppuccin.png to catppuccin-mocha.png
For consistency
2024-05-03 12:50:53 -07:00
Chev 7d0a7186e8 Add new Catppuccin theme screenshots 2024-05-03 12:49:45 -07:00
45 changed files with 917 additions and 171 deletions
+2 -1
View File
@@ -2,9 +2,10 @@ FROM alpine:3.19
ARG TARGETOS ARG TARGETOS
ARG TARGETARCH ARG TARGETARCH
ARG TARGETVARIANT
WORKDIR /app WORKDIR /app
COPY build/glance-$TARGETOS-$TARGETARCH /app/glance COPY build/glance-$TARGETOS-$TARGETARCH${TARGETVARIANT} /app/glance
EXPOSE 8080/tcp EXPOSE 8080/tcp
ENTRYPOINT ["/app/glance"] ENTRYPOINT ["/app/glance"]
+7
View File
@@ -0,0 +1,7 @@
FROM alpine:3.19
WORKDIR /app
COPY build/glance /app/glance
EXPOSE 8080/tcp
ENTRYPOINT ["/app/glance"]
+29 -6
View File
@@ -16,6 +16,7 @@
* iframe * iframe
* Twitch channels & top games * Twitch channels & top games
* GitHub releases * GitHub releases
* Repository overview
* Site monitor * Site monitor
#### Themeable #### Themeable
@@ -41,16 +42,16 @@ Checkout the [configuration docs](docs/configuration.md) to learn more. A [preco
#### Manual #### Manual
Checkout the [releases page](https://github.com/glanceapp/glance/releases) for available binaries. You can place the binary inside `/opt/glance/` and have it start with your server via a [systemd service](https://linuxhandbook.com/create-systemd-services/). To specify a different path for the config file use the `--config` option: Checkout the [releases page](https://github.com/glanceapp/glance/releases) for available binaries. You can place the binary inside `/opt/glance/` and have it start with your server via a [systemd service](https://linuxhandbook.com/create-systemd-services/). To specify a different path for the config file use the `--config` option:
``` ```bash
/opt/glance/glance --config /etc/glance.yml /opt/glance/glance --config /etc/glance.yml
``` ```
#### Docker #### Docker
> [!IMPORTANT] > [!IMPORTANT]
> >
> Make sure you have a valid `glance.yml` file before running the container. > Make sure you have a valid `glance.yml` file in the same directory before running the container.
```console ```bash
docker run -d -p 8080:8080 \ docker run -d -p 8080:8080 \
-v ./glance.yml:/app/glance.yml \ -v ./glance.yml:/app/glance.yml \
-v /etc/timezone:/etc/timezone:ro \ -v /etc/timezone:/etc/timezone:ro \
@@ -79,12 +80,34 @@ Requirements: [Go](https://go.dev/dl/) >= v1.22
To build: To build:
``` ```bash
go build . go build -o build/glance .
``` ```
To run: To run:
``` ```bash
go run . go run .
``` ```
### Building Docker image
Build Glance with CGO disabled:
```bash
CGO_ENABLED=0 go build -o build/glance .
```
Build the image:
**Make sure to replace "owner" with your name or organization.**
```bash
docker build -t owner/glance:latest -f Dockerfile.single-platform .
```
Push the image to your registry:
```bash
docker push owner/glance:latest
```
+130 -20
View File
@@ -14,6 +14,7 @@
- [Weather](#weather) - [Weather](#weather)
- [Monitor](#monitor) - [Monitor](#monitor)
- [Releases](#releases) - [Releases](#releases)
- [Repository](#repository)
- [Bookmarks](#bookmarks) - [Bookmarks](#bookmarks)
- [Calendar](#calendar) - [Calendar](#calendar)
- [Stocks](#stocks) - [Stocks](#stocks)
@@ -155,7 +156,7 @@ Say you have a directory `glance-assets` with a file `gitea-icon.png` in it and
assets-path: /home/user/glance-assets assets-path: /home/user/glance-assets
``` ```
To be able to point to an asset from your assets path, use the the `/assets/` path like such: To be able to point to an asset from your assets path, use the `/assets/` path like such:
```yaml ```yaml
icon: /assets/gitea-icon.png icon: /assets/gitea-icon.png
@@ -250,11 +251,12 @@ pages:
``` ```
### Properties ### Properties
| Name | Type | Required | | Name | Type | Required | Default |
| ---- | ---- | -------- | | ---- | ---- | -------- | ------- |
| title | string | yes | | title | string | yes | |
| slug | string | no | | slug | string | no | |
| columns | array | yes | | show-mobile-header | boolean | no | false |
| columns | array | yes | |
#### `title` #### `title`
The name of the page which gets shown in the navigation bar. The name of the page which gets shown in the navigation bar.
@@ -262,6 +264,13 @@ The name of the page which gets shown in the navigation bar.
#### `slug` #### `slug`
The URL friendly version of the title which is used to access the page. For example if the title of the page is "RSS Feeds" you can make the page accessible via `localhost:8080/feeds` by setting the slug to `feeds`. If not defined, it will automatically be generated from the title. The URL friendly version of the title which is used to access the page. For example if the title of the page is "RSS Feeds" you can make the page accessible via `localhost:8080/feeds` by setting the slug to `feeds`. If not defined, it will automatically be generated from the title.
#### `show-mobile-header`
Whether to show a header displaying the name of the page on mobile. The header purposefully has a lot of vertical whitespace in order to push the content down and make it easier to reach on tall devices.
Preview:
![](images/mobile-header-preview.png)
### Columns ### Columns
Columns are defined for each page using a `columns` property. There are two types of columns - `full` and `small`, which refers to their width. A small column takes up a fixed amount of width (300px) and a full column takes up the all of the remaining width. You can have up to 3 columns per page and you must have either 1 or 2 full columns. Example: Columns are defined for each page using a `columns` property. There are two types of columns - `full` and `small`, which refers to their width. A small column takes up a fixed amount of width (300px) and a full column takes up the all of the remaining width. You can have up to 3 columns per page and you must have either 1 or 2 full columns. Example:
@@ -384,6 +393,8 @@ Example:
| ---- | ---- | -------- | ------- | | ---- | ---- | -------- | ------- |
| style | string | no | vertical-list | | style | string | no | vertical-list |
| feeds | array | yes | | feeds | array | yes |
| thumbnail-height | float | no | 10 |
| card-height | float | no | 27 |
| limit | integer | no | 25 | | limit | integer | no | 25 |
| collapse-after | integer | no | 5 | | collapse-after | integer | no | 5 |
@@ -398,6 +409,16 @@ Used to change the appearance of the widget. Possible values are `vertical-list`
![preview of horizontal-cards style for RSS widget](images/rss-feed-horizontal-cards-preview.png) ![preview of horizontal-cards style for RSS widget](images/rss-feed-horizontal-cards-preview.png)
`horizontal-cards-2`
![preview of horizontal-cards-2 style for RSS widget](images/rss-widget-horizontal-cards-2-preview.png)
##### `thumbnail-height`
Used to modify the height of the thumbnails. Works only when the style is set to `horizontal-cards`. The default value is `10` and the units are `rem`, if you want to for example double the height of the thumbnails you can set it to `20`.
##### `card-height`
Used to modify the height of cards when using the `horizontal-cards-2` style. The default value is `27` and the units are `rem`.
##### `feeds` ##### `feeds`
An array of RSS/atom feeds. The title can optionally be changed. An array of RSS/atom feeds. The title can optionally be changed.
@@ -434,6 +455,7 @@ Preview:
| ---- | ---- | -------- | ------- | | ---- | ---- | -------- | ------- |
| channels | array | yes | | | channels | array | yes | |
| limit | integer | no | 25 | | limit | integer | no | 25 |
| style | string | no | horizontal-cards |
| video-url-template | string | no | https://www.youtube.com/watch?v={VIDEO-ID} | | video-url-template | string | no | https://www.youtube.com/watch?v={VIDEO-ID} |
##### `channels` ##### `channels`
@@ -448,6 +470,13 @@ Then scroll down and click on "Share channel", then "Copy channel ID":
##### `limit` ##### `limit`
The maximum number of videos to show. The maximum number of videos to show.
##### `style`
Used to change the appearance of the widget. Possible values are `horizontal-cards` and `grid-cards`.
Preview of `grid-cards`:
![](images/videos-widget-grid-cards-preview.png)
##### `video-url-template` ##### `video-url-template`
Used to replace the default link for videos. Useful when you're running your own YouTube front-end. Example: Used to replace the default link for videos. Useful when you're running your own YouTube front-end. Example:
@@ -479,6 +508,8 @@ Preview:
| limit | integer | no | 15 | | limit | integer | no | 15 |
| collapse-after | integer | no | 5 | | collapse-after | integer | no | 5 |
| comments-url-template | string | no | https://news.ycombinator.com/item?id={POST-ID} | | comments-url-template | string | no | https://news.ycombinator.com/item?id={POST-ID} |
| sort-by | string | no | top |
| extra-sort-by | string | no | |
##### `comments-url-template` ##### `comments-url-template`
Used to replace the default link for post comments. Useful if you want to use an alternative front-end. Example: Used to replace the default link for post comments. Useful if you want to use an alternative front-end. Example:
@@ -491,12 +522,20 @@ Placeholders:
`{POST-ID}` - the ID of the post `{POST-ID}` - the ID of the post
##### `sort-by`
Used to specify the order in which the posts should get returned. Possible values are `top`, `new`, and `best`.
##### `extra-sort-by`
Can be used to specify an additional sort which will be applied on top of the already sorted posts. By default does not apply any extra sorting and the only available option is `engagement`.
The `engagement` sort tries to place the posts with the most points and comments on top, also prioritizing recent over old posts.
### Reddit ### Reddit
Display a list of posts from a specific subreddit. Display a list of posts from a specific subreddit.
> [!WARNING] > [!WARNING]
> >
> Reddit does not allow unauthorized API access from VPS IPs, if you're hosting Glance on a VPS you will get a 403 response. As a workaround you can route the traffic from Glance through a VPN. > Reddit does not allow unauthorized API access from VPS IPs, if you're hosting Glance on a VPS you will get a 403 response. As a workaround you can route the traffic from Glance through a VPN or your own HTTP proxy using the `request-url-template` property.
Example: Example:
@@ -515,6 +554,10 @@ Example:
| collapse-after | integer | no | 5 | | collapse-after | integer | no | 5 |
| comments-url-template | string | no | https://www.reddit.com/{POST-PATH} | | comments-url-template | string | no | https://www.reddit.com/{POST-PATH} |
| request-url-template | string | no | | | request-url-template | string | no | |
| sort-by | string | no | hot |
| top-period | string | no | day |
| search | string | no | |
| extra-sort-by | string | no | |
##### `subreddit` ##### `subreddit`
The subreddit for which to fetch the posts from. The subreddit for which to fetch the posts from.
@@ -580,6 +623,22 @@ https://proxy/{REQUEST-URL}
https://your.proxy/?url={REQUEST-URL} https://your.proxy/?url={REQUEST-URL}
``` ```
##### `sort-by`
Can be used to specify the order in which the posts should get returned. Possible values are `hot`, `new`, `top` and `rising`.
##### `top-perid`
Available only when `sort-by` is set to `top`. Possible values are `hour`, `day`, `week`, `month`, `year` and `all`.
##### `search`
Keywords to search for. Searching within specific fields is also possible, **though keep in mind that Reddit may remove the ability to use any of these at any time**:
![](images/reddit-field-search.png)
##### `extra-sort-by`
Can be used to specify an additional sort which will be applied on top of the already sorted posts. By default does not apply any extra sorting and the only available option is `engagement`.
The `engagement` sort tries to place the posts with the most points and comments on top, also prioritizing recent over old posts.
### Weather ### Weather
Display weather information for a specific location. The data is provided by https://open-meteo.com/. Display weather information for a specific location. The data is provided by https://open-meteo.com/.
@@ -593,7 +652,7 @@ Example:
> [!NOTE] > [!NOTE]
> >
> US cities which have common names can have their state specified as the second parameter like such: > US cities which have common names can have their state specified as the second parameter as such:
> >
> * Greenville, North Carolina, United States > * Greenville, North Carolina, United States
> * Greenville, South Carolina, United States > * Greenville, South Carolina, United States
@@ -675,7 +734,11 @@ You can hover over the "ERROR" text to view more information.
| Name | Type | Required | | Name | Type | Required |
| ---- | ---- | -------- | | ---- | ---- | -------- |
| sites | array | yes | | | sites | array | yes |
| style | string | no |
##### `style`
To make the widget scale appropriately in a `full` size column, set the style to the experimental `dynamic-columns-experimental` option.
##### `sites` ##### `sites`
@@ -694,7 +757,7 @@ The title used to indicate the site.
`url` `url`
The URL which will be requested and its response will determine the status of the site. The URL which will be requested and its response will determine the status of the site. Optionally, you can specify this using an environment variable with the syntax `${VARIABLE_NAME}`.
`icon` `icon`
@@ -763,6 +826,43 @@ The maximum number of releases to show.
#### `collapse-after` #### `collapse-after`
How many releases are visible before the "SHOW MORE" button appears. Set to `-1` to never collapse. How many releases are visible before the "SHOW MORE" button appears. Set to `-1` to never collapse.
### Repository
Display general information about a repository as well as a list of the latest open pull requests and issues.
Example:
```yaml
- type: repository
repository: glanceapp/glance
pull-requests-limit: 5
issues-limit: 3
```
Preview:
![](images/repository-preview.png)
#### Properties
| Name | Type | Required | Default |
| ---- | ---- | -------- | ------- |
| repository | string | yes | |
| token | string | no | |
| pull-requests-limit | integer | no | 3 |
| issues-limit | integer | no | 3 |
##### `repository`
The owner and repository name that will have their information displayed.
##### `token`
Without authentication Github allows for up to 60 requests per hour. You can easily exceed this limit and start seeing errors if your cache time is low or you have many instances of this widget. To circumvent this you can [create a read only token from your Github account](https://github.com/settings/personal-access-tokens/new) and provide it here.
##### `pull-requests-limit`
The maximum number of latest open pull requests to show. Set to `-1` to not show any.
##### `issues-limit`
The maximum number of latest open issues to show. Set to `-1` to not show any.
### Bookmarks ### Bookmarks
Display a list of links which can be grouped. Display a list of links which can be grouped.
@@ -812,10 +912,14 @@ Preview:
| Name | Type | Required | | Name | Type | Required |
| ---- | ---- | -------- | | ---- | ---- | -------- |
| groups | array | yes | | groups | array | yes |
| style | string | no |
##### `groups` ##### `groups`
An array of groups which can optionally have a title and a custom color. An array of groups which can optionally have a title and a custom color.
##### `style`
To make the widget scale appropriately in a `full` size column, set the style to the experimental `dynamic-columns-experimental` option.
###### Properties for each group ###### Properties for each group
| Name | Type | Required | Default | | Name | Type | Required | Default |
| ---- | ---- | -------- | ------- | | ---- | ---- | -------- | ------- |
@@ -883,18 +987,12 @@ Example:
name: S&P 500 name: S&P 500
- symbol: BTC-USD - symbol: BTC-USD
name: Bitcoin name: Bitcoin
chart-link: https://www.tradingview.com/chart/?symbol=INDEX:BTCUSD
- symbol: NVDA - symbol: NVDA
name: NVIDIA name: NVIDIA
- symbol: AAPL - symbol: AAPL
symbol-link: https://www.google.com/search?tbm=nws&q=apple
name: Apple name: Apple
- symbol: MSFT
name: Microsoft
- symbol: GOOGL
name: Google
- symbol: AMD
name: AMD
- symbol: RDDT
name: Reddit
``` ```
Preview: Preview:
@@ -907,15 +1005,24 @@ Preview:
| ---- | ---- | -------- | | ---- | ---- | -------- |
| stocks | array | yes | | stocks | array | yes |
| sort-by | string | no | | sort-by | string | no |
| style | string | no |
##### `stocks` ##### `stocks`
An array of stocks for which to display information about. An array of stocks for which to display information about.
##### `sort-by`
By default the stocks are displayed in the order they were defined. You can customize their ordering by setting the `sort-by` property to `absolute-change` for descending order based on the stock's absolute price change.
##### `style`
To make the widget scale appropriately in a `full` size column, set the style to the experimental `dynamic-columns-experimental` option.
###### Properties for each stock ###### Properties for each stock
| Name | Type | Required | | Name | Type | Required |
| ---- | ---- | -------- | | ---- | ---- | -------- |
| symbol | string | yes | | symbol | string | yes |
| name | string | no | | name | string | no |
| symbol-link | string | no |
| chart-link | string | no |
`symbol` `symbol`
@@ -925,8 +1032,11 @@ The symbol, as seen in Yahoo Finance.
The name that will be displayed under the symbol. The name that will be displayed under the symbol.
##### `sort-by` `symbol-link`
By default the stocks are displayed in the order they were defined. You can customize their ordering by setting the `sort-by` property to `absolute-change` for descending order based on the stock's absolute price change. The link to go to when clicking on the symbol.
`chart-link`
The link to go to when clicking on the chart.
### Twitch Channels ### Twitch Channels
Display a list of channels from Twitch. Display a list of channels from Twitch.
Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 531 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 365 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

Before

Width:  |  Height:  |  Size: 341 KiB

After

Width:  |  Height:  |  Size: 341 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

+36 -2
View File
@@ -11,8 +11,30 @@ theme:
contrast-multiplier: 1.1 contrast-multiplier: 1.1
``` ```
### Catppuccin ### Catppuccin Frappe
![screenshot](images/themes/catppuccin.png) ![screenshot](images/themes/catppuccin-frappe.png)
```yaml
theme:
background-color: 229 19 23
contrast-multiplier: 1.2
primary-color: 222 74 74
positive-color: 96 44 68
negative-color: 359 68 71
```
### Catppuccin Macchiato
![screenshot](images/themes/catppuccin-macchiato.png)
```yaml
theme:
background-color: 232 23 18
contrast-multiplier: 1.2
primary-color: 220 83 75
positive-color: 105 48 72
negative-color: 351 74 73
```
### Catppuccin Mocha
![screenshot](images/themes/catppuccin-mocha.png)
```yaml ```yaml
theme: theme:
background-color: 240 21 15 background-color: 240 21 15
@@ -42,6 +64,18 @@ theme:
## Light ## Light
### Catppuccin Latte
![screenshot](images/themes/catppuccin-latte.png)
```yaml
theme:
light: true
background-color: 220 23 95
contrast-multiplier: 1.0
primary-color: 220 91 54
positive-color: 109 58 40
negative-color: 347 87 44
```
### Peachy ### Peachy
![screenshot](images/themes/peachy.png) ![screenshot](images/themes/peachy.png)
```yaml ```yaml
+143 -17
View File
@@ -102,12 +102,13 @@
.list { --list-half-gap: 0rem; } .list { --list-half-gap: 0rem; }
.list-gap-2 { --list-half-gap: 0.1rem; } .list-gap-2 { --list-half-gap: 0.1rem; }
.list-gap-4 { --list-half-gap: 0.2rem; } .list-gap-4 { --list-half-gap: 0.2rem; }
.list-gap-10 { --list-half-gap: 0.5rem; }
.list-gap-14 { --list-half-gap: 0.7rem; } .list-gap-14 { --list-half-gap: 0.7rem; }
.list-gap-20 { --list-half-gap: 1rem; } .list-gap-20 { --list-half-gap: 1rem; }
.list-gap-24 { --list-half-gap: 1.2rem; } .list-gap-24 { --list-half-gap: 1.2rem; }
.list > *:not(:first-child) { .list > *:not(:first-child) {
margin-top: calc(var(--list-half-gap) * 2 + 1px); margin-top: calc(var(--list-half-gap) * 2);
} }
.list-with-separator > *:not(:first-child) { .list-with-separator > *:not(:first-child) {
@@ -205,11 +206,29 @@
margin: 0; margin: 0;
} }
hr {
border: 0;
height: 1px;
background-color: var(--color-separator);
}
img, svg { img, svg {
display: block; display: block;
max-width: 100%; max-width: 100%;
} }
img[loading=lazy].loaded:not(.finished-transition) {
transition: opacity .4s;
}
img[loading=lazy].cached:not(.finished-transition) {
transition: none;
}
img[loading=lazy]:not(.loaded, .cached) {
opacity: 0;
}
html { html {
scrollbar-color: var(--color-text-subdue) transparent; scrollbar-color: var(--color-text-subdue) transparent;
scroll-behavior: smooth; scroll-behavior: smooth;
@@ -314,6 +333,44 @@ body {
padding: 0 var(--content-bounds-padding); padding: 0 var(--content-bounds-padding);
} }
.dynamic-columns {
gap: calc(var(--widget-content-vertical-padding) / 2);
display: grid;
grid-template-columns: repeat(var(--columns-per-row), 1fr);
margin: calc(0px - var(--widget-content-vertical-padding) / 2) calc(0px - var(--widget-content-horizontal-padding) / 2);
}
.dynamic-columns > * {
padding: calc(var(--widget-content-vertical-padding) / 2) calc(var(--widget-content-horizontal-padding) / 1.5);
background-color: var(--color-background);
border-radius: var(--border-radius);
}
.dynamic-columns:has(> :nth-child(1)) { --columns-per-row: 1; }
.dynamic-columns:has(> :nth-child(2)) { --columns-per-row: 2; }
.dynamic-columns:has(> :nth-child(3)) { --columns-per-row: 3; }
.dynamic-columns:has(> :nth-child(4)) { --columns-per-row: 4; }
.dynamic-columns:has(> :nth-child(5)) { --columns-per-row: 5; }
@container widget (max-width: 1500px) {
.dynamic-columns:has(> :nth-child(1)) { --columns-per-row: 1; }
.dynamic-columns:has(> :nth-child(2)) { --columns-per-row: 2; }
.dynamic-columns:has(> :nth-child(3)) { --columns-per-row: 3; }
.dynamic-columns:has(> :nth-child(4)) { --columns-per-row: 4; }
}
@container widget (max-width: 1250px) {
.dynamic-columns:has(> :nth-child(1)) { --columns-per-row: 1; }
.dynamic-columns:has(> :nth-child(2)) { --columns-per-row: 2; }
.dynamic-columns:has(> :nth-child(3)) { --columns-per-row: 3; }
}
@container widget (max-width: 850px) {
.dynamic-columns:has(> :nth-child(1)) { --columns-per-row: 1; }
.dynamic-columns:has(> :nth-child(2)) { --columns-per-row: 2; }
}
@container widget (max-width: 550px) {
.dynamic-columns:has(> :nth-child(1)) { --columns-per-row: 1; }
}
.cards-vertical { .cards-vertical {
flex-direction: column; flex-direction: column;
} }
@@ -322,30 +379,44 @@ body {
--cards-per-row: 6.5; --cards-per-row: 6.5;
} }
.cards-grid { .cards-horizontal, .cards-vertical {
--cards-per-row: 6;
}
.cards-horizontal, .cards-vertical, .cards-grid {
--cards-gap: calc(var(--widget-content-vertical-padding) * 0.7); --cards-gap: calc(var(--widget-content-vertical-padding) * 0.7);
display: flex; display: flex;
gap: var(--cards-gap); gap: var(--cards-gap);
} }
.card {
display: flex;
flex-direction: column;
}
.cards-horizontal .card {
flex-shrink: 0;
width: calc(100% / var(--cards-per-row) - var(--cards-gap) * (var(--cards-per-row) - 1) / var(--cards-per-row));
}
.cards-grid .card {
min-width: 0;
}
.cards-horizontal { .cards-horizontal {
overflow-x: auto; overflow-x: auto;
padding-bottom: 1rem; padding-bottom: 1rem;
} }
.cards-grid { .cards-grid {
flex-wrap: wrap; --cards-per-row: 6;
display: grid;
grid-template-columns: repeat(var(--cards-per-row), 1fr);
gap: calc(var(--widget-content-vertical-padding) * 0.7);
} }
@container widget (max-width: 1300px) { .cards-horizontal { --cards-per-row: 5.5; } } @container widget (max-width: 1300px) { .cards-horizontal { --cards-per-row: 5.5; } }
@container widget (max-width: 1100px) { .cards-horizontal { --cards-per-row: 4.5; } } @container widget (max-width: 1100px) { .cards-horizontal { --cards-per-row: 4.5; } }
@container widget (max-width: 850px) { .cards-horizontal { --cards-per-row: 3.5; } } @container widget (max-width: 850px) { .cards-horizontal { --cards-per-row: 3.5; } }
@container widget (max-width: 750px) { .cards-horizontal { --cards-per-row: 3.5; } } @container widget (max-width: 750px) { .cards-horizontal { --cards-per-row: 3.5; } }
@container widget (max-width: 650px) { .cards-horizontal { --cards-per-row: 2.2; } } @container widget (max-width: 650px) { .cards-horizontal { --cards-per-row: 2.5; } }
@container widget (max-width: 450px) { .cards-horizontal { --cards-per-row: 2.3; } }
@container widget (max-width: 1300px) { .cards-grid { --cards-per-row: 5; } } @container widget (max-width: 1300px) { .cards-grid { --cards-per-row: 5; } }
@container widget (max-width: 1100px) { .cards-grid { --cards-per-row: 4; } } @container widget (max-width: 1100px) { .cards-grid { --cards-per-row: 4; } }
@@ -353,12 +424,7 @@ body {
@container widget (max-width: 750px) { .cards-grid { --cards-per-row: 3; } } @container widget (max-width: 750px) { .cards-grid { --cards-per-row: 3; } }
@container widget (max-width: 650px) { .cards-grid { --cards-per-row: 2; } } @container widget (max-width: 650px) { .cards-grid { --cards-per-row: 2; } }
.card {
flex-shrink: 0;
width: calc(100% / var(--cards-per-row) - var(--cards-gap) * (var(--cards-per-row) - 1) / var(--cards-per-row));
display: flex;
flex-direction: column;
}
.widget-error-header { .widget-error-header {
display: flex; display: flex;
@@ -490,7 +556,7 @@ body {
animation-delay: 150ms; animation-delay: 150ms;
} }
.mobile-navigation { .mobile-navigation, .mobile-reachability-header {
display: none; display: none;
} }
@@ -517,6 +583,10 @@ body {
width: 6.5rem; width: 6.5rem;
} }
.stock-chart svg {
width: 100%;
}
.stock-values { .stock-values {
min-width: 8rem; min-width: 8rem;
} }
@@ -553,7 +623,7 @@ body {
.video-thumbnail { .video-thumbnail {
width: 100%; width: 100%;
aspect-ratio: 16 / 9; aspect-ratio: 16 / 8.9;
object-fit: cover; object-fit: cover;
border-radius: var(--border-radius) var(--border-radius) 0 0; border-radius: var(--border-radius) var(--border-radius) 0 0;
} }
@@ -788,6 +858,7 @@ body {
} }
.monitor-site-status-icon { .monitor-site-status-icon {
flex-shrink: 0;
margin-left: auto; margin-left: auto;
width: 2rem; width: 2rem;
height: 2rem; height: 2rem;
@@ -805,11 +876,48 @@ body {
} }
.rss-card-image { .rss-card-image {
height: 10rem; height: var(--rss-thumbnail-height, 10rem);
object-fit: cover; object-fit: cover;
border-radius: var(--border-radius) var(--border-radius) 0 0; border-radius: var(--border-radius) var(--border-radius) 0 0;
} }
.rss-card-2 {
position: relative;
height: var(--rss-card-height, 27rem);
overflow: hidden;
}
.rss-card-2::before {
content: '';
position: absolute;
inset: 0;
pointer-events: none;
background-image: linear-gradient(
0deg,
var(--color-widget-background),
hsla(var(--color-widget-background-hsl-values), 0.8) 6rem, transparent 14rem
);
z-index: 2;
}
.rss-card-2-image {
position: absolute;
width: 100%;
height: 100%;
object-fit: cover;
/* +1px is required to fix some weird graphical bug where the image overflows on the bottom in firefox */
border-radius: calc(var(--border-radius) + 1px);
opacity: 0.9;
z-index: 1;
}
.rss-card-2-content {
position: absolute;
inset-inline: 0;
bottom: var(--widget-content-vertical-padding);
z-index: 3;
}
.twitch-category-thumbnail { .twitch-category-thumbnail {
width: 5rem; width: 5rem;
border-radius: var(--border-radius); border-radius: var(--border-radius);
@@ -1013,6 +1121,8 @@ body {
--content-bounds-padding: 10px; --content-bounds-padding: 10px;
} }
.dynamic-columns:has(> :nth-child(1)) { --columns-per-row: 1; }
.forum-post-list-item { .forum-post-list-item {
flex-flow: row-reverse; flex-flow: row-reverse;
} }
@@ -1020,6 +1130,15 @@ body {
.hide-on-mobile { .hide-on-mobile {
display: none display: none
} }
.mobile-reachability-header {
display: block;
font-size: 3rem;
padding: 10dvh 1rem;
text-align: center;
color: var(--color-text-highlight);
animation: pageColumnsEntrance .3s cubic-bezier(0.25, 1, 0.5, 1) backwards;
}
} }
.size-h1 { font-size: var(--font-size-h1); } .size-h1 { font-size: var(--font-size-h1); }
@@ -1041,6 +1160,8 @@ body {
.text-left { text-align: left; } .text-left { text-align: left; }
.text-right { text-align: right; } .text-right { text-align: right; }
.text-center { text-align: center; } .text-center { text-align: center; }
.text-elevate { margin-top: -0.2em; }
.text-compact { word-spacing: -0.18em; }
.rtl { direction: rtl; } .rtl { direction: rtl; }
.shrink { flex-shrink: 1; } .shrink { flex-shrink: 1; }
.shrink-0 { flex-shrink: 0; } .shrink-0 { flex-shrink: 0; }
@@ -1069,6 +1190,11 @@ body {
.margin-top-7 { margin-top: 0.7rem; } .margin-top-7 { margin-top: 0.7rem; }
.margin-top-10 { margin-top: 1rem; } .margin-top-10 { margin-top: 1rem; }
.margin-top-15 { margin-top: 1.5rem; } .margin-top-15 { margin-top: 1.5rem; }
.margin-block-3 { margin-block: 0.3rem; }
.margin-block-5 { margin-block: 0.5rem; }
.margin-block-7 { margin-block: 0.7rem; }
.margin-block-10 { margin-block: 1rem; }
.margin-block-15 { margin-block: 1.5rem; }
.margin-bottom-3 { margin-bottom: 0.3rem; } .margin-bottom-3 { margin-bottom: 0.3rem; }
.margin-bottom-5 { margin-bottom: 0.5rem; } .margin-bottom-5 { margin-bottom: 0.5rem; }
.margin-bottom-7 { margin-bottom: 0.7rem; } .margin-bottom-7 { margin-bottom: 0.7rem; }
+28
View File
@@ -142,6 +142,33 @@ function setupDynamicRelativeTime() {
}); });
} }
function setupLazyImages() {
const images = document.querySelectorAll("img[loading=lazy]");
if (images.length == 0) {
return;
}
function imageFinishedTransition(image) {
image.classList.add("finished-transition");
}
for (let i = 0; i < images.length; i++) {
const image = images[i];
if (image.complete) {
image.classList.add("cached");
setTimeout(() => imageFinishedTransition(image), 5);
} else {
// TODO: also handle error event
image.addEventListener("load", () => {
image.classList.add("loaded");
setTimeout(() => imageFinishedTransition(image), 500);
});
}
}
}
async function setupPage() { async function setupPage() {
const pageElement = document.getElementById("page"); const pageElement = document.getElementById("page");
const pageContents = await fetchPageContents(pageData.slug); const pageContents = await fetchPageContents(pageData.slug);
@@ -152,6 +179,7 @@ async function setupPage() {
document.body.classList.add("animate-element-transition"); document.body.classList.add("animate-element-transition");
}, 150); }, 150);
setTimeout(setupLazyImages, 5);
setupCarousels(); setupCarousels();
setupDynamicRelativeTime(); setupDynamicRelativeTime();
} }
+5 -2
View File
@@ -22,13 +22,16 @@ var (
RedditCardsHorizontalTemplate = compileTemplate("reddit-horizontal-cards.html", "widget-base.html") RedditCardsHorizontalTemplate = compileTemplate("reddit-horizontal-cards.html", "widget-base.html")
RedditCardsVerticalTemplate = compileTemplate("reddit-vertical-cards.html", "widget-base.html") RedditCardsVerticalTemplate = compileTemplate("reddit-vertical-cards.html", "widget-base.html")
ReleasesTemplate = compileTemplate("releases.html", "widget-base.html") ReleasesTemplate = compileTemplate("releases.html", "widget-base.html")
VideosTemplate = compileTemplate("videos.html", "widget-base.html") VideosTemplate = compileTemplate("videos.html", "widget-base.html", "video-card-contents.html")
VideosGridTemplate = compileTemplate("videos-grid.html", "widget-base.html", "video-card-contents.html")
StocksTemplate = compileTemplate("stocks.html", "widget-base.html") StocksTemplate = compileTemplate("stocks.html", "widget-base.html")
RSSListTemplate = compileTemplate("rss-list.html", "widget-base.html") RSSListTemplate = compileTemplate("rss-list.html", "widget-base.html")
RSSCardsTemplate = compileTemplate("rss-cards.html", "widget-base.html") RSSHorizontalCardsTemplate = compileTemplate("rss-horizontal-cards.html", "widget-base.html")
RSSHorizontalCards2Template = compileTemplate("rss-horizontal-cards-2.html", "widget-base.html")
MonitorTemplate = compileTemplate("monitor.html", "widget-base.html") MonitorTemplate = compileTemplate("monitor.html", "widget-base.html")
TwitchGamesListTemplate = compileTemplate("twitch-games-list.html", "widget-base.html") TwitchGamesListTemplate = compileTemplate("twitch-games-list.html", "widget-base.html")
TwitchChannelsTemplate = compileTemplate("twitch-channels.html", "widget-base.html") TwitchChannelsTemplate = compileTemplate("twitch-channels.html", "widget-base.html")
RepositoryTemplate = compileTemplate("repository.html", "widget-base.html")
) )
var globalTemplateFunctions = template.FuncMap{ var globalTemplateFunctions = template.FuncMap{
+27 -13
View File
@@ -1,23 +1,37 @@
{{ template "widget-base.html" . }} {{ template "widget-base.html" . }}
{{ define "widget-content" }} {{ define "widget-content" }}
{{ if ne .Style "dynamic-columns-experimental" }}
<ul class="list list-gap-24 list-with-separator"> <ul class="list list-gap-24 list-with-separator">
{{ range .Groups }} {{ range .Groups }}
<li class="bookmarks-group"{{ if .Color }} style="--bookmarks-group-color: {{ .Color.AsCSSValue }}"{{ end }}> <li class="bookmarks-group"{{ if .Color }} style="--bookmarks-group-color: {{ .Color.AsCSSValue }}"{{ end }}>
{{ if ne .Title "" }}<div class="bookmarks-group-title size-h3 margin-bottom-3">{{ .Title }}</div>{{ end }} {{ template "group" . }}
<ul class="list list-gap-2">
{{ range .Links }}
<li class="flex items-center gap-10">
{{ if ne "" .Icon }}
<div class="bookmarks-icon-container">
<img class="bookmarks-icon{{ if .IsSimpleIcon }} simple-icon{{ end }}" src="{{ .Icon }}" alt="" loading="lazy">
</div>
{{ end }}
<a href="{{ .URL }}" class="bookmarks-link {{ if .HideArrow }}bookmarks-link-no-arrow {{ end }}color-highlight size-h4" {{ if not .SameTab }}target="_blank"{{ end }} rel="noreferrer">{{ .Title }}</a>
</li>
{{ end }}
</ul>
</li> </li>
{{ end }} {{ end }}
</ul> </ul>
{{ else }}
<div class="dynamic-columns">
{{ range .Groups }}
<div class="bookmarks-group"{{ if .Color }} style="--bookmarks-group-color: {{ .Color.AsCSSValue }}"{{ end }}>
{{ template "group" . }}
</div>
{{ end }}
</div>
{{ end }}
{{ end }}
{{ define "group" }}
{{ if ne .Title "" }}<div class="bookmarks-group-title size-h3 margin-bottom-3">{{ .Title }}</div>{{ end }}
<ul class="list list-gap-2">
{{ range .Links }}
<li class="flex items-center gap-10">
{{ if ne "" .Icon }}
<div class="bookmarks-icon-container">
<img class="bookmarks-icon{{ if .IsSimpleIcon }} simple-icon{{ end }}" src="{{ .Icon }}" alt="" loading="lazy">
</div>
{{ end }}
<a href="{{ .URL }}" class="bookmarks-link {{ if .HideArrow }}bookmarks-link-no-arrow {{ end }}color-highlight size-h4" {{ if not .SameTab }}target="_blank"{{ end }} rel="noreferrer">{{ .Title }}</a>
</li>
{{ end }}
</ul>
{{ end }} {{ end }}
+4
View File
@@ -1,3 +1,7 @@
{{ if .Page.ShowMobileHeader }}
<div class="mobile-reachability-header">{{ .Page.Title }}</div>
{{ end }}
<div class="page-columns"> <div class="page-columns">
{{ range .Page.Columns }} {{ range .Page.Columns }}
<div class="page-column page-column-{{ .Size }}"> <div class="page-column page-column-{{ .Size }}">
+43 -29
View File
@@ -1,39 +1,53 @@
{{ template "widget-base.html" . }} {{ template "widget-base.html" . }}
{{ define "widget-content" }} {{ define "widget-content" }}
{{ if ne .Style "dynamic-columns-experimental" }}
<ul class="list list-gap-20 list-with-separator"> <ul class="list list-gap-20 list-with-separator">
{{ range .Sites }} {{ range .Sites }}
<li class="monitor-site flex items-center gap-15"> <li class="monitor-site flex items-center gap-15">
{{ if .IconUrl }} {{ template "site" . }}
<img class="monitor-site-icon" src="{{ .IconUrl }}" alt="" loading="lazy">
{{ end }}
<div>
<a class="size-h3 color-highlight" href="{{ .Url }}" {{ if not .SameTab }}target="_blank"{{ end }} rel="noreferrer">{{ .Title }}</a>
<ul class="list-horizontal-text">
{{ if not .Status.Error }}
<li>{{ .StatusText }}</li>
<li>{{ .Status.ResponseTime.Milliseconds | formatNumber }}ms</li>
{{ else if .Status.TimedOut }}
<li class="color-negative">Timed Out</li>
{{ else }}
<li class="color-negative" title="{{ .Status.Error }}">ERROR</li>
{{ end }}
</ul>
</div>
{{ if eq .StatusStyle "good" }}
<div class="monitor-site-status-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="var(--color-positive)">
<path fill-rule="evenodd" d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm13.36-1.814a.75.75 0 1 0-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 0 0-1.06 1.06l2.25 2.25a.75.75 0 0 0 1.14-.094l3.75-5.25Z" clip-rule="evenodd" />
</svg>
</div>
{{ else }}
<div class="monitor-site-status-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="var(--color-negative)">
<path fill-rule="evenodd" d="M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003ZM12 8.25a.75.75 0 0 1 .75.75v3.75a.75.75 0 0 1-1.5 0V9a.75.75 0 0 1 .75-.75Zm0 8.25a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Z" clip-rule="evenodd" />
</svg>
</div>
{{ end }}
</li> </li>
{{ end }} {{ end }}
</ul> </ul>
{{ else }}
<ul class="dynamic-columns">
{{ range .Sites }}
<div class="flex items-center gap-15">
{{ template "site" . }}
</div>
{{ end }}
</ul>
{{ end }}
{{ end }}
{{ define "site" }}
{{ if .IconUrl }}
<img class="monitor-site-icon" src="{{ .IconUrl }}" alt="" loading="lazy">
{{ end }}
<div>
<a class="size-h3 color-highlight" href="{{ .Url }}" {{ if not .SameTab }}target="_blank"{{ end }} rel="noreferrer">{{ .Title }}</a>
<ul class="list-horizontal-text">
{{ if not .Status.Error }}
<li>{{ .StatusText }}</li>
<li>{{ .Status.ResponseTime.Milliseconds | formatNumber }}ms</li>
{{ else if .Status.TimedOut }}
<li class="color-negative">Timed Out</li>
{{ else }}
<li class="color-negative" title="{{ .Status.Error }}">ERROR</li>
{{ end }}
</ul>
</div>
{{ if eq .StatusStyle "good" }}
<div class="monitor-site-status-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="var(--color-positive)">
<path fill-rule="evenodd" d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm13.36-1.814a.75.75 0 1 0-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 0 0-1.06 1.06l2.25 2.25a.75.75 0 0 0 1.14-.094l3.75-5.25Z" clip-rule="evenodd" />
</svg>
</div>
{{ else }}
<div class="monitor-site-status-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="var(--color-negative)">
<path fill-rule="evenodd" d="M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003ZM12 8.25a.75.75 0 0 1 .75.75v3.75a.75.75 0 0 1-1.5 0V9a.75.75 0 0 1 .75-.75Zm0 8.25a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Z" clip-rule="evenodd" />
</svg>
</div>
{{ end }}
{{ end }} {{ end }}
+1 -1
View File
@@ -1,7 +1,7 @@
{{ template "widget-base.html" . }} {{ template "widget-base.html" . }}
{{ define "widget-content" }} {{ define "widget-content" }}
<ul class="list list-gap-14 list-collapsible"> <ul class="list list-gap-10 list-collapsible">
{{ range $i, $release := .Releases }} {{ range $i, $release := .Releases }}
<li {{ if shouldCollapse $i $.CollapseAfter }}class="list-collapsible-item" style="--animation-delay: {{ itemAnimationDelay $i $.CollapseAfter }};"{{ end }}> <li {{ if shouldCollapse $i $.CollapseAfter }}class="list-collapsible-item" style="--animation-delay: {{ itemAnimationDelay $i $.CollapseAfter }};"{{ end }}>
<a class="size-h4 block text-truncate color-primary-if-not-visited" href="{{ $release.NotesUrl }}" target="_blank" rel="noreferrer">{{ .Name }}</a> <a class="size-h4 block text-truncate color-primary-if-not-visited" href="{{ $release.NotesUrl }}" target="_blank" rel="noreferrer">{{ .Name }}</a>
+44
View File
@@ -0,0 +1,44 @@
{{ template "widget-base.html" . }}
{{ define "widget-content" }}
<a class="size-h4 color-highlight" href="https://github.com/{{ $.RepositoryDetails.Name }}" target="_blank" rel="noreferrer">{{ .RepositoryDetails.Name }}</a>
<ul class="list-horizontal-text">
<li>{{ .RepositoryDetails.Stars | formatNumber }} stars</li>
<li>{{ .RepositoryDetails.Forks | formatNumber }} forks</li>
</ul>
{{ if gt (len .RepositoryDetails.PullRequests) 0 }}
<hr class="margin-block-10">
<a class="text-compact" href="https://github.com/{{ $.RepositoryDetails.Name }}/pulls" target="_blank" rel="noreferrer">Open pull requests ({{ .RepositoryDetails.OpenPullRequests | formatNumber }} total)</a>
<div class="flex gap-7 size-h5 margin-top-3">
<ul class="list list-gap-2">
{{ range .RepositoryDetails.PullRequests }}
<li title="{{ .CreatedAt | formatTime }}" {{ dynamicRelativeTimeAttrs .CreatedAt }}>{{ .CreatedAt | relativeTime }}</li>
{{ end }}
</ul>
<ul class="list list-gap-2 min-width-0">
{{ range .RepositoryDetails.PullRequests }}
<li><a class="color-primary-if-not-visited text-truncate block" title="{{ .Title }}" target="_blank" rel="noreferrer" href="https://github.com/{{ $.RepositoryDetails.Name }}/pull/{{ .Number }}">{{ .Title }}</a></li>
{{ end }}
</ul>
</div>
{{ end }}
{{ if gt (len .RepositoryDetails.Issues) 0 }}
<hr class="margin-block-10">
<a class="text-compact" href="https://github.com/{{ $.RepositoryDetails.Name }}/issues" target="_blank" rel="noreferrer">Open issues ({{ .RepositoryDetails.OpenIssues | formatNumber }} total)</a>
<div class="flex gap-7 size-h5 margin-top-3">
<ul class="list list-gap-2">
{{ range .RepositoryDetails.Issues }}
<li title="{{ .CreatedAt | formatTime }}" {{ dynamicRelativeTimeAttrs .CreatedAt }}>{{ .CreatedAt | relativeTime }}</li>
{{ end }}
</ul>
<ul class="list list-gap-2 min-width-0">
{{ range .RepositoryDetails.Issues }}
<li><a class="color-primary-if-not-visited text-truncate block" title="{{ .Title }}" target="_blank" rel="noreferrer" href="https://github.com/{{ $.RepositoryDetails.Name }}/issues/{{ .Number }}">{{ .Title }}</a></li>
{{ end }}
</ul>
</div>
{{ end }}
{{ end }}
@@ -0,0 +1,28 @@
{{ template "widget-base.html" . }}
{{ define "widget-content-classes" }}widget-content-frameless{{ end }}
{{ define "widget-content" }}
<div class="carousel-container">
<div class="cards-horizontal carousel-items-container"{{ if ne 0.0 .CardHeight }} style="--rss-card-height: {{ .CardHeight }}rem;"{{ end }}>
{{ range .Items }}
<div class="card rss-card-2 widget-content-frame thumbnail-container">
{{ if ne "" .ImageURL }}
<img class="rss-card-2-image thumbnail" loading="lazy" src="{{ .ImageURL }}" alt="">
{{ else }}
<svg class="rss-card-2-image" style="transform: scale(0.35) translateY(-25%)" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="var(--color-text-subdue)">
<path stroke-linecap="round" stroke-linejoin="round" d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" />
</svg>
{{ end }}
<div class="rss-card-2-content padding-inline-widget">
<a href="{{ .Link }}" title="{{ .Title }}" class="block text-truncate color-primary-if-not-visited" target="_blank" rel="noreferrer">{{ .Title }}</a>
<ul class="list-horizontal-text flex-nowrap margin-top-5">
<li class="shrink-0" title="{{ .PublishedAt | formatTime }}" {{ dynamicRelativeTimeAttrs .PublishedAt }}>{{ .PublishedAt | relativeTime }}</li>
<li class="shrink min-width-0 text-truncate">{{ .ChannelName }}</li>
</ul>
</div>
</div>
{{ end }}
</div>
</div>
{{ end }}
@@ -4,7 +4,7 @@
{{ define "widget-content" }} {{ define "widget-content" }}
<div class="carousel-container"> <div class="carousel-container">
<div class="cards-horizontal carousel-items-container"> <div class="cards-horizontal carousel-items-container"{{ if ne 0.0 .ThumbnailHeight }} style="--rss-thumbnail-height: {{ .ThumbnailHeight }}rem;"{{ end }}>
{{ range .Items }} {{ range .Items }}
<div class="card widget-content-frame thumbnail-container"> <div class="card widget-content-frame thumbnail-container">
{{ if ne "" .ImageURL }} {{ if ne "" .ImageURL }}
+29 -13
View File
@@ -1,23 +1,39 @@
{{ template "widget-base.html" . }} {{ template "widget-base.html" . }}
{{ define "widget-content" }} {{ define "widget-content" }}
{{ if ne .Style "dynamic-columns-experimental" }}
<ul class="list list-gap-20 list-with-separator"> <ul class="list list-gap-20 list-with-separator">
{{ range .Stocks }} {{ range .Stocks }}
<li class="flex items-center gap-15"> <li class="flex items-center gap-15">
<div class="shrink min-width-0"> {{ template "stock" . }}
<div class="color-highlight size-h3 text-truncate">{{ .Symbol }}</div>
<div class="text-truncate">{{ .Name }}</div>
</div>
<svg class="stock-chart shrink-0" viewBox="0 0 100 50">
<polyline fill="none" stroke="var(--color-text-subdue)" stroke-width="1.5px" points="{{ .SvgChartPoints }}" vector-effect="non-scaling-stroke"></polyline>
</svg>
<div class="stock-values shrink-0">
<div class="size-h3 text-right {{ if eq .PercentChange 0.0 }}{{ else if gt .PercentChange 0.0 }}color-positive{{ else }}color-negative{{ end }}">{{ printf "%+.2f" .PercentChange }}%</div>
<div class="text-right">{{ .Currency }}{{ .Price | formatPrice }}</div>
</div>
</li> </li>
{{ end }} {{ end }}
</ul> </ul>
{{ else }}
<div class="dynamic-columns">
{{ range .Stocks }}
<div class="flex items-center gap-15">
{{ template "stock" . }}
</div>
{{ end }}
</div>
{{ end }}
{{ end }}
{{ define "stock" }}
<div class="shrink min-width-0">
<a{{ if ne "" .SymbolLink }} href="{{ .SymbolLink }}" target="_blank" rel="noreferrer"{{ end }} class="color-highlight size-h3 block text-truncate">{{ .Symbol }}</a>
<div class="text-truncate">{{ .Name }}</div>
</div>
<a class="stock-chart" {{ if ne "" .ChartLink }} href="{{ .ChartLink }}" target="_blank" rel="noreferrer"{{ end }}>
<svg class="stock-chart shrink-0" viewBox="0 0 100 50">
<polyline fill="none" stroke="var(--color-text-subdue)" stroke-width="1.5px" points="{{ .SvgChartPoints }}" vector-effect="non-scaling-stroke"></polyline>
</svg>
</a>
<div class="stock-values shrink-0">
<div class="size-h3 text-right {{ if eq .PercentChange 0.0 }}{{ else if gt .PercentChange 0.0 }}color-positive{{ else }}color-negative{{ end }}">{{ printf "%+.2f" .PercentChange }}%</div>
<div class="text-right">{{ .Currency }}{{ .Price | formatPrice }}</div>
</div>
{{ end }} {{ end }}
@@ -0,0 +1,12 @@
{{ define "video-card-contents" }}
<img class="video-thumbnail thumbnail" loading="lazy" src="{{ .ThumbnailUrl }}" alt="">
<div class="margin-top-10 margin-bottom-widget flex flex-column grow padding-inline-widget">
<a class="video-title color-primary-if-not-visited" href="{{ .Url }}" target="_blank" rel="noreferrer" title="{{ .Title }}">{{ .Title }}</a>
<ul class="list-horizontal-text flex-nowrap margin-top-7">
<li class="shrink-0" title="{{ .TimePosted | formatTime }}" {{ dynamicRelativeTimeAttrs .TimePosted }}>{{ .TimePosted | relativeTime }}</li>
<li class="shrink min-width-0">
<a class="block text-truncate" href="{{ .AuthorUrl }}" target="_blank" rel="noreferrer">{{ .Author }}</a>
</li>
</ul>
</div>
{{ end }}
@@ -0,0 +1,13 @@
{{ template "widget-base.html" . }}
{{ define "widget-content-classes" }}widget-content-frameless{{ end }}
{{ define "widget-content" }}
<div class="cards-grid">
{{ range .Videos }}
<div class="card widget-content-frame thumbnail-container">
{{ template "video-card-contents" . }}
</div>
{{ end }}
</div>
{{ end }}
+2 -11
View File
@@ -4,19 +4,10 @@
{{ define "widget-content" }} {{ define "widget-content" }}
<div class="carousel-container"> <div class="carousel-container">
<div class="videos cards-horizontal carousel-items-container"> <div class="cards-horizontal carousel-items-container">
{{ range .Videos }} {{ range .Videos }}
<div class="card widget-content-frame thumbnail-container"> <div class="card widget-content-frame thumbnail-container">
<img class="video-thumbnail thumbnail" loading="lazy" src="{{ .ThumbnailUrl }}" alt=""> {{ template "video-card-contents" . }}
<div class="margin-top-10 margin-bottom-widget flex flex-column grow padding-inline-widget">
<a class="video-title color-primary-if-not-visited" href="{{ .Url }}" target="_blank" rel="noreferrer" title="{{ .Title }}">{{ .Title }}</a>
<ul class="list-horizontal-text flex-nowrap margin-top-7">
<li class="shrink-0" title="{{ .TimePosted | formatTime }}" {{ dynamicRelativeTimeAttrs .TimePosted }}>{{ .TimePosted | relativeTime }}</li>
<li class="shrink min-width-0">
<a class="block text-truncate" href="{{ .AuthorUrl }}" target="_blank" rel="noreferrer">{{ .Author }}</a>
</li>
</ul>
</div>
</div> </div>
{{ end }} {{ end }}
</div> </div>
+131
View File
@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"log/slog" "log/slog"
"net/http" "net/http"
"sync"
"time" "time"
) )
@@ -115,3 +116,133 @@ func FetchLatestReleasesFromGithub(repositories []string, token string) (AppRele
return appReleases, nil return appReleases, nil
} }
type GithubTicket struct {
Number int
CreatedAt time.Time
Title string
}
type RepositoryDetails struct {
Name string
Stars int
Forks int
OpenPullRequests int
PullRequests []GithubTicket
OpenIssues int
Issues []GithubTicket
}
type githubRepositoryDetailsResponseJson struct {
Name string `json:"full_name"`
Stars int `json:"stargazers_count"`
Forks int `json:"forks_count"`
}
type githubTicketResponseJson struct {
Count int `json:"total_count"`
Tickets []struct {
Number int `json:"number"`
CreatedAt string `json:"created_at"`
Title string `json:"title"`
} `json:"items"`
}
func FetchRepositoryDetailsFromGithub(repository string, token string, maxPRs int, maxIssues int) (RepositoryDetails, error) {
repositoryRequest, err := http.NewRequest("GET", fmt.Sprintf("https://api.github.com/repos/%s", repository), nil)
if err != nil {
return RepositoryDetails{}, fmt.Errorf("%w: could not create request with repository: %v", ErrNoContent, err)
}
PRsRequest, _ := http.NewRequest("GET", fmt.Sprintf("https://api.github.com/search/issues?q=is:pr+is:open+repo:%s&per_page=%d", repository, maxPRs), nil)
issuesRequest, _ := http.NewRequest("GET", fmt.Sprintf("https://api.github.com/search/issues?q=is:issue+is:open+repo:%s&per_page=%d", repository, maxIssues), nil)
if token != "" {
token = fmt.Sprintf("Bearer %s", token)
repositoryRequest.Header.Add("Authorization", token)
PRsRequest.Header.Add("Authorization", token)
issuesRequest.Header.Add("Authorization", token)
}
var detailsResponse githubRepositoryDetailsResponseJson
var detailsErr error
var PRsResponse githubTicketResponseJson
var PRsErr error
var issuesResponse githubTicketResponseJson
var issuesErr error
var wg sync.WaitGroup
wg.Add(1)
go (func() {
defer wg.Done()
detailsResponse, detailsErr = decodeJsonFromRequest[githubRepositoryDetailsResponseJson](defaultClient, repositoryRequest)
})()
if maxPRs > 0 {
wg.Add(1)
go (func() {
defer wg.Done()
PRsResponse, PRsErr = decodeJsonFromRequest[githubTicketResponseJson](defaultClient, PRsRequest)
})()
}
if maxIssues > 0 {
wg.Add(1)
go (func() {
defer wg.Done()
issuesResponse, issuesErr = decodeJsonFromRequest[githubTicketResponseJson](defaultClient, issuesRequest)
})()
}
wg.Wait()
if detailsErr != nil {
return RepositoryDetails{}, fmt.Errorf("%w: could not get repository details: %s", ErrNoContent, detailsErr)
}
details := RepositoryDetails{
Name: detailsResponse.Name,
Stars: detailsResponse.Stars,
Forks: detailsResponse.Forks,
PullRequests: make([]GithubTicket, 0, len(PRsResponse.Tickets)),
Issues: make([]GithubTicket, 0, len(issuesResponse.Tickets)),
}
err = nil
if maxPRs > 0 {
if PRsErr != nil {
err = fmt.Errorf("%w: could not get PRs: %s", ErrPartialContent, PRsErr)
} else {
details.OpenPullRequests = PRsResponse.Count
for i := range PRsResponse.Tickets {
details.PullRequests = append(details.PullRequests, GithubTicket{
Number: PRsResponse.Tickets[i].Number,
CreatedAt: parseGithubTime(PRsResponse.Tickets[i].CreatedAt),
Title: PRsResponse.Tickets[i].Title,
})
}
}
}
if maxIssues > 0 {
if issuesErr != nil {
// TODO: fix, overwriting the previous error
err = fmt.Errorf("%w: could not get issues: %s", ErrPartialContent, issuesErr)
} else {
details.OpenIssues = issuesResponse.Count
for i := range issuesResponse.Tickets {
details.Issues = append(details.Issues, GithubTicket{
Number: issuesResponse.Tickets[i].Number,
CreatedAt: parseGithubTime(issuesResponse.Tickets[i].CreatedAt),
Title: issuesResponse.Tickets[i].Title,
})
}
}
}
return details, err
}
+4 -4
View File
@@ -18,8 +18,8 @@ type hackerNewsPostResponseJson struct {
TimePosted int64 `json:"time"` TimePosted int64 `json:"time"`
} }
func getHackerNewsTopPostIds() ([]int, error) { func getHackerNewsPostIds(sort string) ([]int, error) {
request, _ := http.NewRequest("GET", "https://hacker-news.firebaseio.com/v0/topstories.json", nil) request, _ := http.NewRequest("GET", fmt.Sprintf("https://hacker-news.firebaseio.com/v0/%sstories.json", sort), nil)
response, err := decodeJsonFromRequest[[]int](defaultClient, request) response, err := decodeJsonFromRequest[[]int](defaultClient, request)
if err != nil { if err != nil {
@@ -83,8 +83,8 @@ func getHackerNewsPostsFromIds(postIds []int, commentsUrlTemplate string) (Forum
return posts, nil return posts, nil
} }
func FetchHackerNewsTopPosts(limit int, commentsUrlTemplate string) (ForumPosts, error) { func FetchHackerNewsPosts(sort string, limit int, commentsUrlTemplate string) (ForumPosts, error) {
postIds, err := getHackerNewsTopPostIds() postIds, err := getHackerNewsPostIds(sort)
if err != nil { if err != nil {
return nil, err return nil, err
+8 -6
View File
@@ -85,12 +85,14 @@ var currencyToSymbol = map[string]string{
} }
type Stock struct { type Stock struct {
Name string Name string `yaml:"name"`
Symbol string Symbol string `yaml:"symbol"`
Currency string ChartLink string `yaml:"chart-link"`
Price float64 SymbolLink string `yaml:"symbol-link"`
PercentChange float64 Currency string `yaml:"-"`
SvgChartPoints string Price float64 `yaml:"-"`
PercentChange float64 `yaml:"-"`
SvgChartPoints string `yaml:"-"`
} }
type Stocks []Stock type Stocks []Stock
+20 -5
View File
@@ -30,12 +30,29 @@ type subredditResponseJson struct {
} `json:"data"` } `json:"data"`
} }
func FetchSubredditPosts(subreddit string, commentsUrlTemplate string, requestUrlTemplate string) (ForumPosts, error) { func FetchSubredditPosts(subreddit, sort, topPeriod, search, commentsUrlTemplate, requestUrlTemplate string) (ForumPosts, error) {
subreddit = url.QueryEscape(subreddit) query := url.Values{}
requestUrl := fmt.Sprintf("https://www.reddit.com/r/%s/hot.json", subreddit) var requestUrl string
if search != "" {
query.Set("q", search+" subreddit:"+subreddit)
query.Set("sort", sort)
}
if sort == "top" {
query.Set("t", topPeriod)
}
if search != "" {
requestUrl = fmt.Sprintf("https://www.reddit.com/search.json?%s", query.Encode())
} else {
requestUrl = fmt.Sprintf("https://www.reddit.com/r/%s/%s.json?%s", subreddit, sort, query.Encode())
}
if requestUrlTemplate != "" { if requestUrlTemplate != "" {
requestUrl = strings.ReplaceAll(requestUrlTemplate, "{REQUEST-URL}", requestUrl) requestUrl = strings.ReplaceAll(requestUrlTemplate, "{REQUEST-URL}", requestUrl)
} }
request, err := http.NewRequest("GET", requestUrl, nil) request, err := http.NewRequest("GET", requestUrl, nil)
if err != nil { if err != nil {
@@ -93,7 +110,5 @@ func FetchSubredditPosts(subreddit string, commentsUrlTemplate string, requestUr
posts = append(posts, forumPost) posts = append(posts, forumPost)
} }
posts.CalculateEngagement()
return posts, nil return posts, nil
} }
+1 -1
View File
@@ -28,7 +28,7 @@ func extractDomainFromUrl(u string) string {
return "" return ""
} }
return strings.TrimPrefix(parsed.Host, "www.") return strings.TrimPrefix(strings.ToLower(parsed.Host), "www.")
} }
func SvgPolylineCoordsFromYValues(width float64, height float64, values []float64) string { func SvgPolylineCoordsFromYValues(width float64, height float64, values []float64) string {
+7 -10
View File
@@ -24,15 +24,10 @@ type stockResponseJson struct {
} `json:"chart"` } `json:"chart"`
} }
type StockRequest struct {
Symbol string
Name string
}
// TODO: allow changing chart time frame // TODO: allow changing chart time frame
const stockChartDays = 21 const stockChartDays = 21
func FetchStocksDataFromYahoo(stockRequests []StockRequest) (Stocks, error) { func FetchStocksDataFromYahoo(stockRequests Stocks) (Stocks, error) {
requests := make([]*http.Request, 0, len(stockRequests)) requests := make([]*http.Request, 0, len(stockRequests))
for i := range stockRequests { for i := range stockRequests {
@@ -86,10 +81,12 @@ func FetchStocksDataFromYahoo(stockRequests []StockRequest) (Stocks, error) {
} }
stocks = append(stocks, Stock{ stocks = append(stocks, Stock{
Name: stockRequests[i].Name, Name: stockRequests[i].Name,
Symbol: response.Chart.Result[0].Meta.Symbol, Symbol: response.Chart.Result[0].Meta.Symbol,
Price: response.Chart.Result[0].Meta.RegularMarketPrice, SymbolLink: stockRequests[i].SymbolLink,
Currency: currency, ChartLink: stockRequests[i].ChartLink,
Price: response.Chart.Result[0].Meta.RegularMarketPrice,
Currency: currency,
PercentChange: percentChange( PercentChange: percentChange(
response.Chart.Result[0].Meta.RegularMarketPrice, response.Chart.Result[0].Meta.RegularMarketPrice,
previous, previous,
+5 -4
View File
@@ -55,10 +55,11 @@ type templateData struct {
} }
type Page struct { type Page struct {
Title string `yaml:"name"` Title string `yaml:"name"`
Slug string `yaml:"slug"` Slug string `yaml:"slug"`
Columns []Column `yaml:"columns"` ShowMobileHeader bool `yaml:"show-mobile-header"`
mu sync.Mutex Columns []Column `yaml:"columns"`
mu sync.Mutex
} }
func (p *Page) UpdateOutdatedWidgets() { func (p *Page) UpdateOutdatedWidgets() {
+1
View File
@@ -22,6 +22,7 @@ type Bookmarks struct {
HideArrow bool `yaml:"hide-arrow"` HideArrow bool `yaml:"hide-arrow"`
} `yaml:"links"` } `yaml:"links"`
} `yaml:"groups"` } `yaml:"groups"`
Style string `yaml:"style"`
} }
func (widget *Bookmarks) Initialize() error { func (widget *Bookmarks) Initialize() error {
+11 -3
View File
@@ -13,6 +13,8 @@ type HackerNews struct {
widgetBase `yaml:",inline"` widgetBase `yaml:",inline"`
Posts feed.ForumPosts `yaml:"-"` Posts feed.ForumPosts `yaml:"-"`
Limit int `yaml:"limit"` Limit int `yaml:"limit"`
SortBy string `yaml:"sort-by"`
ExtraSortBy string `yaml:"extra-sort-by"`
CollapseAfter int `yaml:"collapse-after"` CollapseAfter int `yaml:"collapse-after"`
CommentsUrlTemplate string `yaml:"comments-url-template"` CommentsUrlTemplate string `yaml:"comments-url-template"`
ShowThumbnails bool `yaml:"-"` ShowThumbnails bool `yaml:"-"`
@@ -29,18 +31,24 @@ func (widget *HackerNews) Initialize() error {
widget.CollapseAfter = 5 widget.CollapseAfter = 5
} }
if widget.SortBy != "top" && widget.SortBy != "new" && widget.SortBy != "best" {
widget.SortBy = "top"
}
return nil return nil
} }
func (widget *HackerNews) Update(ctx context.Context) { func (widget *HackerNews) Update(ctx context.Context) {
posts, err := feed.FetchHackerNewsTopPosts(40, widget.CommentsUrlTemplate) posts, err := feed.FetchHackerNewsPosts(widget.SortBy, 40, widget.CommentsUrlTemplate)
if !widget.canContinueUpdateAfterHandlingErr(err) { if !widget.canContinueUpdateAfterHandlingErr(err) {
return return
} }
posts.CalculateEngagement() if widget.ExtraSortBy == "engagement" {
posts.SortByEngagement() posts.CalculateEngagement()
posts.SortByEngagement()
}
if widget.Limit < len(posts) { if widget.Limit < len(posts) {
posts = posts[:widget.Limit] posts = posts[:widget.Limit]
+9 -8
View File
@@ -46,14 +46,15 @@ func statusCodeToStyle(status int) string {
type Monitor struct { type Monitor struct {
widgetBase `yaml:",inline"` widgetBase `yaml:",inline"`
Sites []struct { Sites []struct {
Title string `yaml:"title"` Title string `yaml:"title"`
Url string `yaml:"url"` Url OptionalEnvString `yaml:"url"`
IconUrl string `yaml:"icon"` IconUrl string `yaml:"icon"`
SameTab bool `yaml:"same-tab"` SameTab bool `yaml:"same-tab"`
Status *feed.SiteStatus `yaml:"-"` Status *feed.SiteStatus `yaml:"-"`
StatusText string `yaml:"-"` StatusText string `yaml:"-"`
StatusStyle string `yaml:"-"` StatusStyle string `yaml:"-"`
} `yaml:"sites"` } `yaml:"sites"`
Style string `yaml:"style"`
} }
func (widget *Monitor) Initialize() error { func (widget *Monitor) Initialize() error {
@@ -66,7 +67,7 @@ func (widget *Monitor) Update(ctx context.Context) {
requests := make([]*http.Request, len(widget.Sites)) requests := make([]*http.Request, len(widget.Sites))
for i := range widget.Sites { for i := range widget.Sites {
request, err := http.NewRequest("GET", widget.Sites[i].Url, nil) request, err := http.NewRequest("GET", string(widget.Sites[i].Url), nil)
if err != nil { if err != nil {
message := fmt.Errorf("failed to create http request for %s: %s", widget.Sites[i].Url, err) message := fmt.Errorf("failed to create http request for %s: %s", widget.Sites[i].Url, err)
+42 -2
View File
@@ -17,6 +17,10 @@ type Reddit struct {
Subreddit string `yaml:"subreddit"` Subreddit string `yaml:"subreddit"`
Style string `yaml:"style"` Style string `yaml:"style"`
ShowThumbnails bool `yaml:"show-thumbnails"` ShowThumbnails bool `yaml:"show-thumbnails"`
SortBy string `yaml:"sort-by"`
TopPeriod string `yaml:"top-period"`
Search string `yaml:"search"`
ExtraSortBy string `yaml:"extra-sort-by"`
CommentsUrlTemplate string `yaml:"comments-url-template"` CommentsUrlTemplate string `yaml:"comments-url-template"`
Limit int `yaml:"limit"` Limit int `yaml:"limit"`
CollapseAfter int `yaml:"collapse-after"` CollapseAfter int `yaml:"collapse-after"`
@@ -36,6 +40,14 @@ func (widget *Reddit) Initialize() error {
widget.CollapseAfter = 5 widget.CollapseAfter = 5
} }
if !isValidRedditSortType(widget.SortBy) {
widget.SortBy = "hot"
}
if !isValidRedditTopPeriod(widget.TopPeriod) {
widget.TopPeriod = "day"
}
if widget.RequestUrlTemplate != "" { if widget.RequestUrlTemplate != "" {
if !strings.Contains(widget.RequestUrlTemplate, "{REQUEST-URL}") { if !strings.Contains(widget.RequestUrlTemplate, "{REQUEST-URL}") {
return errors.New("no `{REQUEST-URL}` placeholder specified") return errors.New("no `{REQUEST-URL}` placeholder specified")
@@ -47,8 +59,32 @@ func (widget *Reddit) Initialize() error {
return nil return nil
} }
func isValidRedditSortType(sortBy string) bool {
return sortBy == "hot" ||
sortBy == "new" ||
sortBy == "top" ||
sortBy == "rising"
}
func isValidRedditTopPeriod(period string) bool {
return period == "hour" ||
period == "day" ||
period == "week" ||
period == "month" ||
period == "year" ||
period == "all"
}
func (widget *Reddit) Update(ctx context.Context) { func (widget *Reddit) Update(ctx context.Context) {
posts, err := feed.FetchSubredditPosts(widget.Subreddit, widget.CommentsUrlTemplate, widget.RequestUrlTemplate) // TODO: refactor, use a struct to pass all of these
posts, err := feed.FetchSubredditPosts(
widget.Subreddit,
widget.SortBy,
widget.TopPeriod,
widget.Search,
widget.CommentsUrlTemplate,
widget.RequestUrlTemplate,
)
if !widget.canContinueUpdateAfterHandlingErr(err) { if !widget.canContinueUpdateAfterHandlingErr(err) {
return return
@@ -58,7 +94,11 @@ func (widget *Reddit) Update(ctx context.Context) {
posts = posts[:widget.Limit] posts = posts[:widget.Limit]
} }
posts.SortByEngagement() if widget.ExtraSortBy == "engagement" {
posts.CalculateEngagement()
posts.SortByEngagement()
}
widget.Posts = posts widget.Posts = posts
} }
+52
View File
@@ -0,0 +1,52 @@
package widget
import (
"context"
"html/template"
"time"
"github.com/glanceapp/glance/internal/assets"
"github.com/glanceapp/glance/internal/feed"
)
type Repository struct {
widgetBase `yaml:",inline"`
RequestedRepository string `yaml:"repository"`
Token OptionalEnvString `yaml:"token"`
PullRequestsLimit int `yaml:"pull-requests-limit"`
IssuesLimit int `yaml:"issues-limit"`
RepositoryDetails feed.RepositoryDetails
}
func (widget *Repository) Initialize() error {
widget.withTitle("Repository").withCacheDuration(1 * time.Hour)
if widget.PullRequestsLimit == 0 || widget.PullRequestsLimit < -1 {
widget.PullRequestsLimit = 3
}
if widget.IssuesLimit == 0 || widget.IssuesLimit < -1 {
widget.IssuesLimit = 3
}
return nil
}
func (widget *Repository) Update(ctx context.Context) {
details, err := feed.FetchRepositoryDetailsFromGithub(
widget.RequestedRepository,
string(widget.Token),
widget.PullRequestsLimit,
widget.IssuesLimit,
)
if !widget.canContinueUpdateAfterHandlingErr(err) {
return
}
widget.RepositoryDetails = details
}
func (widget *Repository) Render() template.HTML {
return widget.render(widget, assets.RepositoryTemplate)
}
+21 -7
View File
@@ -10,12 +10,14 @@ import (
) )
type RSS struct { type RSS struct {
widgetBase `yaml:",inline"` widgetBase `yaml:",inline"`
FeedRequests []feed.RSSFeedRequest `yaml:"feeds"` FeedRequests []feed.RSSFeedRequest `yaml:"feeds"`
Style string `yaml:"style"` Style string `yaml:"style"`
Items feed.RSSFeedItems `yaml:"-"` ThumbnailHeight float64 `yaml:"thumbnail-height"`
Limit int `yaml:"limit"` CardHeight float64 `yaml:"card-height"`
CollapseAfter int `yaml:"collapse-after"` Items feed.RSSFeedItems `yaml:"-"`
Limit int `yaml:"limit"`
CollapseAfter int `yaml:"collapse-after"`
} }
func (widget *RSS) Initialize() error { func (widget *RSS) Initialize() error {
@@ -29,6 +31,14 @@ func (widget *RSS) Initialize() error {
widget.CollapseAfter = 5 widget.CollapseAfter = 5
} }
if widget.ThumbnailHeight < 0 {
widget.ThumbnailHeight = 0
}
if widget.CardHeight < 0 {
widget.CardHeight = 0
}
return nil return nil
} }
@@ -48,7 +58,11 @@ func (widget *RSS) Update(ctx context.Context) {
func (widget *RSS) Render() template.HTML { func (widget *RSS) Render() template.HTML {
if widget.Style == "horizontal-cards" { if widget.Style == "horizontal-cards" {
return widget.render(widget, assets.RSSCardsTemplate) return widget.render(widget, assets.RSSHorizontalCardsTemplate)
}
if widget.Style == "horizontal-cards-2" {
return widget.render(widget, assets.RSSHorizontalCards2Template)
} }
return widget.render(widget, assets.RSSListTemplate) return widget.render(widget, assets.RSSListTemplate)
+5 -4
View File
@@ -9,11 +9,12 @@ import (
"github.com/glanceapp/glance/internal/feed" "github.com/glanceapp/glance/internal/feed"
) )
// TODO: rename to Markets at some point
type Stocks struct { type Stocks struct {
widgetBase `yaml:",inline"` widgetBase `yaml:",inline"`
Stocks feed.Stocks `yaml:"-"` Stocks feed.Stocks `yaml:"stocks"`
Sort string `yaml:"sort-by"` Sort string `yaml:"sort-by"`
Tickers []feed.StockRequest `yaml:"stocks"` Style string `yaml:"style"`
} }
func (widget *Stocks) Initialize() error { func (widget *Stocks) Initialize() error {
@@ -23,7 +24,7 @@ func (widget *Stocks) Initialize() error {
} }
func (widget *Stocks) Update(ctx context.Context) { func (widget *Stocks) Update(ctx context.Context) {
stocks, err := feed.FetchStocksDataFromYahoo(widget.Tickers) stocks, err := feed.FetchStocksDataFromYahoo(widget.Stocks)
if !widget.canContinueUpdateAfterHandlingErr(err) { if !widget.canContinueUpdateAfterHandlingErr(err) {
return return
+5
View File
@@ -13,6 +13,7 @@ type Videos struct {
widgetBase `yaml:",inline"` widgetBase `yaml:",inline"`
Videos feed.Videos `yaml:"-"` Videos feed.Videos `yaml:"-"`
VideoUrlTemplate string `yaml:"video-url-template"` VideoUrlTemplate string `yaml:"video-url-template"`
Style string `yaml:"style"`
Channels []string `yaml:"channels"` Channels []string `yaml:"channels"`
Limit int `yaml:"limit"` Limit int `yaml:"limit"`
} }
@@ -42,5 +43,9 @@ func (widget *Videos) Update(ctx context.Context) {
} }
func (widget *Videos) Render() template.HTML { func (widget *Videos) Render() template.HTML {
if widget.Style == "grid-cards" {
return widget.render(widget, assets.VideosGridTemplate)
}
return widget.render(widget, assets.VideosTemplate) return widget.render(widget, assets.VideosTemplate)
} }
+2
View File
@@ -43,6 +43,8 @@ func New(widgetType string) (Widget, error) {
return &TwitchGames{}, nil return &TwitchGames{}, nil
case "twitch-channels": case "twitch-channels":
return &TwitchChannels{}, nil return &TwitchChannels{}, nil
case "repository":
return &Repository{}, nil
default: default:
return nil, fmt.Errorf("unknown widget type: %s", widgetType) return nil, fmt.Errorf("unknown widget type: %s", widgetType)
} }
+9 -1
View File
@@ -67,6 +67,14 @@ var buildTargets = []buildTarget{
arch: "arm", arch: "arm",
armV: 7, armV: 7,
}, },
{
os: "openbsd",
arch: "amd64",
},
{
os: "openbsd",
arch: "386",
},
} }
func main() { func main() {
@@ -113,7 +121,7 @@ func main() {
output, err := exec.Command( output, err := exec.Command(
"sudo", "docker", "build", "sudo", "docker", "build",
"--platform=linux/arm64,linux/amd64", "--platform=linux/amd64,linux/arm64,linux/arm/v7",
"-t", versionTag, "-t", versionTag,
"-t", latestTag, "-t", latestTag,
".", ".",