Compare commits

...
25 Commits
Author SHA1 Message Date
Svilen Markov 7b444b88e3 Merge pull request #35 from glanceapp/v0.3.0
V0.3.0
2024-05-03 05:46:39 +01:00
Svilen Markov f317b1b189 Adjust the height and position of reddit thumbnails 2024-05-03 05:43:08 +01:00
Svilen Markov 3523562c3a Additions to monitor and bookmarks widgets 2024-05-03 05:38:40 +01:00
Svilen Markov 5fdc3e3f10 Merge pull request #23 from jarylc/proxy-option-for-reddit
Added a request-url-template option for Reddit requests
2024-05-03 02:48:32 +01:00
Jaryl Chng 24ef8f6e2a Added a request-url-template option for Reddit requests
Reddit blocks known datacenter IPs from accessing their endpoints unless you request them to unblock it. Even so it has a low chance of success.

The new option will allow users to specify a request URL to handle Reddit requests.
2024-05-03 09:42:47 +08:00
Svilen Markov d8d6625478 Allow specifying state in weather location 2024-05-02 19:54:20 +01:00
Svilen Markov ad06146784 Add hide-on-mobile utility class 2024-05-02 18:45:58 +01:00
Svilen Markov c97171883c Rename thumbnails to show-thumbnails 2024-05-02 18:45:15 +01:00
Svilen Markov 9c5b6045af Don't request unused wind speed data 2024-05-02 17:14:27 +01:00
Svilen Markov eb85a7656f Allow showing thumbnails for reddit posts 2024-05-02 00:34:25 +01:00
Svilen Markov 25d45b309d Update doc 2024-05-02 00:33:18 +01:00
Svilen Markov 7d0b4535bb Change bool to boolean for consistency 2024-05-01 23:55:48 +01:00
Svilen Markov f8af2025e8 Rename flex-grow to grow 2024-05-01 23:47:14 +01:00
Svilen Markov 948c117f5b Retain stocks order by default 2024-05-01 22:28:59 +01:00
Svilen Markov 58967ab758 Update doc 2024-05-01 22:06:40 +01:00
Svilen Markov 44a153d30a Allow using alternative links for YT, HN and reddit 2024-05-01 19:08:25 +01:00
Svilen Markov d5d6103327 Fix currency symbol being hardcoded to $ 2024-04-29 22:32:46 +01:00
Svilen Markov 189b8895b0 Change monitor widget to make GET requests 2024-04-29 17:04:39 +01:00
Svilen Markov 748ad1ff8d Allow setting path to custom CSS 2024-04-29 16:47:18 +01:00
Svilen Markov ccdafcdfab Allow changing between C and F for weather temp 2024-04-29 16:29:11 +01:00
Svilen Markov 78572a534b Increase max body length 2024-04-29 16:18:32 +01:00
Svilen Markov 2eb8e71e89 Update configuration docs 2024-04-29 01:15:28 +01:00
Svilen Markov 5375103c4b Update dependencies 2024-04-28 23:53:32 +01:00
Svilen Markov faeeb7c899 Truncate response body in error message 2024-04-28 14:53:58 +01:00
Svilen Markov 99fd77feac Update incorrect link in configuration doc 2024-04-27 22:48:42 +01:00
29 changed files with 547 additions and 90 deletions
+163 -12
View File
@@ -174,18 +174,19 @@ theme:
``` ```
### Themes ### Themes
If you don't want to spend time configuring your own theme, there are [several available themes](preconfigured-themes.md) which you can simply copy the values for. If you don't want to spend time configuring your own theme, there are [several available themes](themes.md) which you can simply copy the values for.
### Properties ### Properties
| Name | Type | Required | Default | | Name | Type | Required | Default |
| ---- | ---- | -------- | ------- | | ---- | ---- | -------- | ------- |
| light | bool | no | false | | light | boolean | no | false |
| background-color | HSL | no | 240 8 9 | | background-color | HSL | no | 240 8 9 |
| primary-color | HSL | no | 43 50 70 | | primary-color | HSL | no | 43 50 70 |
| positive-color | HSL | no | same as `primary-color` | | positive-color | HSL | no | same as `primary-color` |
| negative-color | HSL | no | 0 70 70 | | negative-color | HSL | no | 0 70 70 |
| contrast-multiplier | number | no | 1 | | contrast-multiplier | number | no | 1 |
| text-saturation-multiplier | number | no | 1 | | text-saturation-multiplier | number | no | 1 |
| custom-css-file | string | no | |
#### `light` #### `light`
Whether the scheme is light or dark. This does not change the background color, it inverts the text colors so that they look appropriately on a light background. Whether the scheme is light or dark. This does not change the background color, it inverts the text colors so that they look appropriately on a light background.
@@ -210,6 +211,23 @@ Used to increase or decrease the contrast (in other words visibility) of the tex
#### `text-saturation-multiplier` #### `text-saturation-multiplier`
Used to increase or decrease the saturation of text, useful when using a custom background color with a high amount of saturation and needing the text to have a more neutral color. `0.5` means that the saturation will be 50% lower and `1.5` means that it'll be 50% higher. Used to increase or decrease the saturation of text, useful when using a custom background color with a high amount of saturation and needing the text to have a more neutral color. `0.5` means that the saturation will be 50% lower and `1.5` means that it'll be 50% higher.
#### `custom-css-file`
Path to a custom CSS file, either external or one from within the server configured assets path. Example:
```yaml
theme:
custom-css-file: /assets/my-style.css
```
> [!TIP]
>
> Because Glance uses a lot of utility classes it might be difficult to target some elements. To make it easier to style specific widgets, each widget has a `widget-type-{name}` class, so for example if you wanted to make the links inside just the RSS widget bigger you could use the following selector:
>
> ```css
> .widget-type-rss a {
> font-size: 1.5rem;
> }
## Pages & Columns ## Pages & Columns
![illustration of pages and columns](images/pages-and-columns-illustration.png) ![illustration of pages and columns](images/pages-and-columns-illustration.png)
@@ -416,6 +434,7 @@ Preview:
| ---- | ---- | -------- | ------- | | ---- | ---- | -------- | ------- |
| channels | array | yes | | | channels | array | yes | |
| limit | integer | no | 25 | | limit | integer | no | 25 |
| video-url-template | string | no | https://www.youtube.com/watch?v={VIDEO-ID} |
##### `channels` ##### `channels`
A list of channel IDs. One way of getting the ID of a channel is going to the channel's page and clicking on its description: A list of channel IDs. One way of getting the ID of a channel is going to the channel's page and clicking on its description:
@@ -429,6 +448,17 @@ 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.
##### `video-url-template`
Used to replace the default link for videos. Useful when you're running your own YouTube front-end. Example:
```yaml
video-url-template: https://invidious.your-domain.com/watch?v={VIDEO-ID}
```
Placeholders:
`{VIDEO-ID}` - the ID of the video
### Hacker News ### Hacker News
Display a list of posts from [Hacker News](https://news.ycombinator.com/). Display a list of posts from [Hacker News](https://news.ycombinator.com/).
@@ -448,10 +478,26 @@ 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`
Used to replace the default link for post comments. Useful if you want to use an alternative front-end. Example:
```yaml
comments-url-template: https://www.hckrnws.com/stories/{POST-ID}
```
Placeholders:
`{POST-ID}` - the ID of the post
### Reddit ### Reddit
Display a list of posts from a specific subreddit. Display a list of posts from a specific subreddit.
> [!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.
Example: Example:
```yaml ```yaml
@@ -464,8 +510,11 @@ Example:
| ---- | ---- | -------- | ------- | | ---- | ---- | -------- | ------- |
| subreddit | string | yes | | | subreddit | string | yes | |
| style | string | no | vertical-list | | style | string | no | vertical-list |
| show-thumbnails | boolean | no | false |
| limit | integer | no | 15 | | limit | integer | no | 15 |
| collapse-after | integer | no | 5 | | collapse-after | integer | no | 5 |
| comments-url-template | string | no | https://www.reddit.com/{POST-PATH} |
| request-url-template | string | no | |
##### `subreddit` ##### `subreddit`
The subreddit for which to fetch the posts from. The subreddit for which to fetch the posts from.
@@ -485,12 +534,52 @@ Used to change the appearance of the widget. Possible values are `vertical-list`
![](images/reddit-widget-vertical-cards-preview.png) ![](images/reddit-widget-vertical-cards-preview.png)
##### `show-thumbnails`
Shows or hides thumbnails next to the post. This only works if the `style` is `vertical-list`. Preview:
![](images/reddit-widget-vertical-list-thumbnails.png)
> [!NOTE]
>
> Thumbnails don't work for some subreddits due to Reddit's API not returning the thumbnail URL. No workaround for this yet.
##### `limit` ##### `limit`
The maximum number of posts to show. The maximum number of posts to show.
##### `collapse-after` ##### `collapse-after`
How many posts are visible before the "SHOW MORE" button appears. Set to `-1` to never collapse. Not available when using the `vertical-cards` and `horizontal-cards` styles. How many posts are visible before the "SHOW MORE" button appears. Set to `-1` to never collapse. Not available when using the `vertical-cards` and `horizontal-cards` styles.
##### `comments-url-template`
Used to replace the default link for post comments. Useful if you want to use the old Reddit design or any other 3rd party front-end. Example:
```yaml
comments-url-template: https://old.reddit.com/{POST-PATH}
```
Placeholders:
`{POST-PATH}` - the full path to the post, such as:
```
r/selfhosted/comments/bsp01i/welcome_to_rselfhosted_please_read_this_first/
```
`{POST-ID}` - the ID that comes after `/comments/`
`{SUBREDDIT}` - the subreddit name
##### `request-url-template`
A custom request url that will be used to fetch the data instead. This is useful when you're hosting Glance on a VPS and Reddit is blocking the requests, and you want to route it through an HTTP proxy.
Placeholders:
`{REQUEST-URL}` - will be templated and replaced with the expanded request URL (i.e. https://www.reddit.com/r/selfhosted/hot.json). Example:
```
https://proxy/{REQUEST-URL}
https://your.proxy/?url={REQUEST-URL}
```
### 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/.
@@ -498,9 +587,19 @@ Example:
```yaml ```yaml
- type: weather - type: weather
units: metric
location: London, United Kingdom location: London, United Kingdom
``` ```
> [!NOTE]
>
> US cities which have common names can have their state specified as the second parameter like such:
>
> * Greenville, North Carolina, United States
> * Greenville, South Carolina, United States
> * Greenville, Mississippi, United States
Preview: Preview:
![](images/weather-widget-preview.png) ![](images/weather-widget-preview.png)
@@ -512,14 +611,32 @@ Each bar represents a 2 hour interval. The yellow background represents sunrise
| Name | Type | Required | Default | | Name | Type | Required | Default |
| ---- | ---- | -------- | ------- | | ---- | ---- | -------- | ------- |
| location | string | yes | | | location | string | yes | |
| units | string | no | metric |
| hide-location | boolean | no | false | | hide-location | boolean | no | false |
| show-area-name | boolean | no | false |
##### `location` ##### `location`
The name of the city and country to fetch weather information for. Attempting to launch the applcation with an invalid location will result in an error. You can use the [gecoding API page](https://open-meteo.com/en/docs/geocoding-api) to search for your specific location. Glance will use the first result from the list if there are multiple. The name of the city and country to fetch weather information for. Attempting to launch the applcation with an invalid location will result in an error. You can use the [gecoding API page](https://open-meteo.com/en/docs/geocoding-api) to search for your specific location. Glance will use the first result from the list if there are multiple.
##### `units`
Whether to show the temperature in celsius or fahrenheit, possible values are `metric` or `imperial`.
##### `hide-location` ##### `hide-location`
Optionally don't display the location name on the widget. Optionally don't display the location name on the widget.
##### `show-area-name`
Whether to display the state/administrative area in the location name. If set to `true` the location will be displayed as:
```
Greenville, North Carolina, United States
```
Otherwise, if set to `false` (which is the default) it'll be displayed as:
```
Greenville, United States
```
### Monitor ### Monitor
Display a list of sites and whether they are reachable (online) or not. This is determined by sending a HEAD request to the specified URL, if the response is 200 then the site is OK. The time it took to receive a response is also shown in milliseconds. Display a list of sites and whether they are reachable (online) or not. This is determined by sending a HEAD request to the specified URL, if the response is 200 then the site is OK. The time it took to receive a response is also shown in milliseconds.
@@ -564,11 +681,12 @@ You can hover over the "ERROR" text to view more information.
Properties for each site: Properties for each site:
| Name | Type | Required | | Name | Type | Required | Default |
| ---- | ---- | -------- | | ---- | ---- | -------- | ------- |
| title | string | yes | | title | string | yes | |
| url | string | yes | | url | string | yes | |
| icon | string | no | | icon | string | no | |
| same-tab | boolean | no | false |
`title` `title`
@@ -582,6 +700,10 @@ The URL which will be requested and its response will determine the status of th
Optional URL to an image which will be used as the icon for the site. Can be an external URL or internal via [server configured assets](#assets-path). Optional URL to an image which will be used as the icon for the site. Can be an external URL or internal via [server configured assets](#assets-path).
`same-tab`
Whether to open the link in the same or a new tab.
### Releases ### Releases
Display a list of releases for specific repositories on Github. Draft releases and prereleases will not be shown. Display a list of releases for specific repositories on Github. Draft releases and prereleases will not be shown.
@@ -698,14 +820,39 @@ An array of groups which can optionally have a title and a custom color.
| Name | Type | Required | Default | | Name | Type | Required | Default |
| ---- | ---- | -------- | ------- | | ---- | ---- | -------- | ------- |
| title | string | no | | | title | string | no | |
| color | HSL | no | the primary theme color | | color | HSL | no | the primary color of the theme |
| links | array | yes | | | links | array | yes | |
###### Properties for each link ###### Properties for each link
| Name | Type | Required | | Name | Type | Required | Default |
| ---- | ---- | -------- | | ---- | ---- | -------- | ------- |
| title | string | yes | | title | string | yes | |
| url | string | yes | | url | string | yes | |
| icon | string | no | |
| same-tab | boolean | no | false |
| hide-arrow | boolean | no | false |
`icon`
URL pointing to an image. You can also directly use [Simple Icons](https://simpleicons.org/) via a `si:` prefix:
```yaml
icon: si:gmail
icon: si:youtube
icon: si:reddit
```
> [!WARNING]
>
> Simple Icons are loaded externally and are hosted on `cdnjs.cloudflare.com`, if you do not wish to depend on a 3rd party you are free to download the icons individually and host them locally.
`same-tab`
Whether to open the link in the same tab or a new one.
`hide-arrow`
Whether to hide the colored arrow on each link.
### Calendar ### Calendar
Display a calendar. Display a calendar.
@@ -759,6 +906,7 @@ Preview:
| Name | Type | Required | | Name | Type | Required |
| ---- | ---- | -------- | | ---- | ---- | -------- |
| stocks | array | yes | | stocks | array | yes |
| sort-by | string | no |
##### `stocks` ##### `stocks`
An array of stocks for which to display information about. An array of stocks for which to display information about.
@@ -777,6 +925,9 @@ 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`
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.
### 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: 82 KiB

+5 -5
View File
@@ -3,17 +3,17 @@ module github.com/glanceapp/glance
go 1.22.0 go 1.22.0
require ( require (
github.com/mmcdole/gofeed v1.3.0
golang.org/x/text v0.14.0 golang.org/x/text v0.14.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
github.com/mmcdole/gofeed v1.3.0
) )
require ( require (
github.com/PuerkitoBio/goquery v1.8.0 // indirect github.com/PuerkitoBio/goquery v1.9.1 // indirect
github.com/andybalholm/cascadia v1.3.1 // indirect github.com/andybalholm/cascadia v1.3.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/mmcdole/goxpp v1.1.1-0.20240225020742-a0c311522b23 // indirect github.com/mmcdole/goxpp v1.1.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
golang.org/x/net v0.4.0 // indirect golang.org/x/net v0.24.0 // indirect
) )
+43 -11
View File
@@ -1,34 +1,66 @@
github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U= github.com/PuerkitoBio/goquery v1.9.1 h1:mTL6XjbJTZdpfL+Gwl5U2h1l9yEkJjhmlTeV9VPW7UI=
github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI= github.com/PuerkitoBio/goquery v1.9.1/go.mod h1:cW1n6TmIMDoORQU5IU/P1T3tGFunOeXEpGP2WHRwkbY=
github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/mmcdole/gofeed v1.3.0 h1:5yn+HeqlcvjMeAI4gu6T+crm7d0anY85+M+v6fIFNG4= github.com/mmcdole/gofeed v1.3.0 h1:5yn+HeqlcvjMeAI4gu6T+crm7d0anY85+M+v6fIFNG4=
github.com/mmcdole/gofeed v1.3.0/go.mod h1:9TGv2LcJhdXePDzxiuMnukhV2/zb6VtnZt1mS+SjkLE= github.com/mmcdole/gofeed v1.3.0/go.mod h1:9TGv2LcJhdXePDzxiuMnukhV2/zb6VtnZt1mS+SjkLE=
github.com/mmcdole/goxpp v1.1.1-0.20240225020742-a0c311522b23 h1:Zr92CAlFhy2gL+V1F+EyIuzbQNbSgP4xhTODZtrXUtk= github.com/mmcdole/goxpp v1.1.1 h1:RGIX+D6iQRIunGHrKqnA2+700XMCnNv0bAOOv5MUhx8=
github.com/mmcdole/goxpp v1.1.1-0.20240225020742-a0c311522b23/go.mod h1:v+25+lT2ViuQ7mVxcncQ8ch1URund48oH+jhjiwEgS8= github.com/mmcdole/goxpp v1.1.1/go.mod h1:v+25+lT2ViuQ7mVxcncQ8ch1URund48oH+jhjiwEgS8=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+48 -3
View File
@@ -33,6 +33,7 @@
--color-widget-background: hsl(var(--color-widget-background-hsl-values)); --color-widget-background: hsl(var(--color-widget-background-hsl-values));
--color-separator: hsl(var(--bghs), calc(var(--scheme) ((var(--scheme) var(--bgl)) + 4% * var(--cm)))); --color-separator: hsl(var(--bghs), calc(var(--scheme) ((var(--scheme) var(--bgl)) + 4% * var(--cm))));
--color-widget-content-border: hsl(var(--bghs), calc(var(--scheme) (var(--scheme) var(--bgl) + 4%))); --color-widget-content-border: hsl(var(--bghs), calc(var(--scheme) (var(--scheme) var(--bgl) + 4%)));
--color-widget-background-highlight: hsl(var(--bghs), calc(var(--scheme) (var(--scheme) var(--bgl) + 4%)));
--ths: var(--bgh), calc(var(--bgs) * var(--tsm)); --ths: var(--bgh), calc(var(--bgs) * var(--tsm));
--color-text-base: hsl(var(--ths), calc(var(--scheme) var(--cm) * 58%)); --color-text-base: hsl(var(--ths), calc(var(--scheme) var(--cm) * 58%));
@@ -80,7 +81,7 @@
.visited-indicator:not(.text-truncate)::after, .visited-indicator:not(.text-truncate)::after,
.visited-indicator.text-truncate::before, .visited-indicator.text-truncate::before,
.bookmarks-link::after { .bookmarks-link:not(.bookmarks-link-no-arrow)::after {
content: '↗'; content: '↗';
margin-left: 0.5em; margin-left: 0.5em;
display: inline-block; display: inline-block;
@@ -567,6 +568,21 @@ body {
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
} }
.forum-post-list-item {
display: flex;
gap: 1.2rem;
}
.forum-post-list-thumbnail {
flex-shrink: 0;
width: 6rem;
height: 4.1rem;
border-radius: var(--border-radius);
object-fit: cover;
border: 1px solid var(--color-separator);
margin-top: 0.1rem;
}
.bookmarks-group { .bookmarks-group {
--bookmarks-group-color: var(--color-primary); --bookmarks-group-color: var(--color-primary);
} }
@@ -575,10 +591,31 @@ body {
color: var(--bookmarks-group-color); color: var(--bookmarks-group-color);
} }
.bookmarks-link::after { .bookmarks-group .bookmarks-link::after {
color: var(--bookmarks-group-color); color: var(--bookmarks-group-color);
} }
.bookmarks-icon-container {
margin-block: 0.1rem;
background-color: var(--color-widget-background-highlight);
border-radius: var(--border-radius);
padding: 0.5rem;
}
.bookmarks-icon {
width: 20px;
height: 20px;
opacity: 0.8;
}
.simple-icon {
opacity: 0.7;
}
:root:not(.light-scheme) .simple-icon {
filter: invert(1);
}
.calendar-day { .calendar-day {
width: calc(100% / 7); width: calc(100% / 7);
text-align: center; text-align: center;
@@ -975,6 +1012,14 @@ body {
--widget-content-horizontal-padding: 10px; --widget-content-horizontal-padding: 10px;
--content-bounds-padding: 10px; --content-bounds-padding: 10px;
} }
.forum-post-list-item {
flex-flow: row-reverse;
}
.hide-on-mobile {
display: none
}
} }
.size-h1 { font-size: var(--font-size-h1); } .size-h1 { font-size: var(--font-size-h1); }
@@ -1011,7 +1056,7 @@ body {
.justify-center { justify-content: center; } .justify-center { justify-content: center; }
.justify-end { justify-content: end; } .justify-end { justify-content: end; }
.uppercase { text-transform: uppercase; } .uppercase { text-transform: uppercase; }
.flex-grow { flex-grow: 1; } .grow { flex-grow: 1; }
.flex-column { flex-direction: column; } .flex-column { flex-direction: column; }
.items-center { align-items: center; } .items-center { align-items: center; }
.items-start { align-items: start; } .items-start { align-items: start; }
+8 -1
View File
@@ -7,7 +7,14 @@
{{ if ne .Title "" }}<div class="bookmarks-group-title size-h3 margin-bottom-3">{{ .Title }}</div>{{ end }} {{ if ne .Title "" }}<div class="bookmarks-group-title size-h3 margin-bottom-3">{{ .Title }}</div>{{ end }}
<ul class="list list-gap-2"> <ul class="list list-gap-2">
{{ range .Links }} {{ range .Links }}
<li><a href="{{ .URL }}" class="bookmarks-link color-highlight size-h4" target="_blank" rel="noreferrer">{{ .Title }}</a></li> <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 }} {{ end }}
</ul> </ul>
</li> </li>
@@ -4,6 +4,21 @@
<ul class="list list-gap-14 list-collapsible"> <ul class="list list-gap-14 list-collapsible">
{{ range $i, $post := .Posts }} {{ range $i, $post := .Posts }}
<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 }}>
<div class="forum-post-list-item thumbnail-container">
{{ if $.ShowThumbnails }}
{{ if ne $post.ThumbnailUrl "" }}
<img class="forum-post-list-thumbnail thumbnail" src="{{ $post.ThumbnailUrl }}" alt="" loading="lazy">
{{ else if $post.HasTargetUrl }}
<svg class="forum-post-list-thumbnail hide-on-mobile" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="-9 -8 40 40" stroke-width="1.5" stroke="var(--color-text-subdue)">
<path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244" />
</svg>
{{ else }}
<svg class="forum-post-list-thumbnail hide-on-mobile" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="-9 -8 40 40" stroke-width="1.5" stroke="var(--color-text-subdue)">
<path stroke-linecap="round" stroke-linejoin="round" d="M7.5 8.25h9m-9 3H12m-9.75 1.51c0 1.6 1.123 2.994 2.707 3.227 1.129.166 2.27.293 3.423.379.35.026.67.21.865.501L12 21l2.755-4.133a1.14 1.14 0 0 1 .865-.501 48.172 48.172 0 0 0 3.423-.379c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0 0 12 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018Z" />
</svg>
{{ end }}
{{ end }}
<div class="grow">
<a href="{{ $post.DiscussionUrl }}" class="size-h3 color-primary-if-not-visited" target="_blank" rel="noreferrer">{{ .Title }}</a> <a href="{{ $post.DiscussionUrl }}" class="size-h3 color-primary-if-not-visited" target="_blank" rel="noreferrer">{{ .Title }}</a>
<ul class="list-horizontal-text"> <ul class="list-horizontal-text">
<li title="{{ $post.TimePosted | formatTime }}" {{ dynamicRelativeTimeAttrs $post.TimePosted }}>{{ $post.TimePosted | relativeTime }}</li> <li title="{{ $post.TimePosted | formatTime }}" {{ dynamicRelativeTimeAttrs $post.TimePosted }}>{{ $post.TimePosted | relativeTime }}</li>
@@ -13,6 +28,8 @@
<li class="shrink min-width-0"><a class="visited-indicator text-truncate block" href="{{ .TargetUrl }}" target="_blank" rel="noreferrer">{{ $post.TargetUrlDomain }}</a></li> <li class="shrink min-width-0"><a class="visited-indicator text-truncate block" href="{{ .TargetUrl }}" target="_blank" rel="noreferrer">{{ $post.TargetUrlDomain }}</a></li>
{{ end }} {{ end }}
</ul> </ul>
</div>
</div>
</li> </li>
{{ end }} {{ end }}
</ul> </ul>
+1 -1
View File
@@ -8,7 +8,7 @@
<img class="monitor-site-icon" src="{{ .IconUrl }}" alt="" loading="lazy"> <img class="monitor-site-icon" src="{{ .IconUrl }}" alt="" loading="lazy">
{{ end }} {{ end }}
<div> <div>
<a class="size-h3 color-highlight" href="{{ .Url }}" target="_blank" rel="noreferrer">{{ .Title }}</a> <a class="size-h3 color-highlight" href="{{ .Url }}" {{ if not .SameTab }}target="_blank"{{ end }} rel="noreferrer">{{ .Title }}</a>
<ul class="list-horizontal-text"> <ul class="list-horizontal-text">
{{ if not .Status.Error }} {{ if not .Status.Error }}
<li>{{ .StatusText }}</li> <li>{{ .StatusText }}</li>
+7 -2
View File
@@ -11,7 +11,12 @@
{{ end }} {{ end }}
{{ define "document-root-attrs" }}{{ if .App.Config.Theme.Light }}class="light-scheme"{{ end }}{{ end }} {{ define "document-root-attrs" }}{{ if .App.Config.Theme.Light }}class="light-scheme"{{ end }}{{ end }}
{{ define "document-head-after" }}{{ template "page-style-overrides.gotmpl" . }}{{ end }} {{ define "document-head-after" }}
{{ template "page-style-overrides.gotmpl" . }}
{{ if ne "" .App.Config.Theme.CustomCSSFile }}
<link rel="stylesheet" href="{{ .App.Config.Theme.CustomCSSFile }}?v={{ .App.Config.Server.StartedAt.Unix }}">
{{ end }}
{{ end }}
{{ define "navigation-links" }} {{ define "navigation-links" }}
{{ range .App.Config.Pages }} {{ range .App.Config.Pages }}
@@ -24,7 +29,7 @@
<div class="header flex padding-inline-widget widget-content-frame"> <div class="header flex padding-inline-widget widget-content-frame">
<!-- TODO: Replace G with actual logo, first need an actual logo --> <!-- TODO: Replace G with actual logo, first need an actual logo -->
<div class="logo">G</div> <div class="logo">G</div>
<div class="nav flex flex-grow"> <div class="nav flex grow">
{{ template "navigation-links" . }} {{ template "navigation-links" . }}
</div> </div>
</div> </div>
@@ -12,7 +12,7 @@
<img class="reddit-card-thumbnail" loading="lazy" src="{{ .ThumbnailUrl }}" alt=""> <img class="reddit-card-thumbnail" loading="lazy" src="{{ .ThumbnailUrl }}" alt="">
</div> </div>
{{ end }} {{ end }}
<div class="padding-widget flex flex-column flex-grow relative"> <div class="padding-widget flex flex-column grow relative">
{{ if ne "" .TargetUrl }} {{ if ne "" .TargetUrl }}
<a class="color-highlight size-h5 text-truncate visited-indicator" href="{{ .TargetUrl }}" target="_blank" rel="noreferrer">{{ .TargetUrlDomain }}</a> <a class="color-highlight size-h5 text-truncate visited-indicator" href="{{ .TargetUrl }}" target="_blank" rel="noreferrer">{{ .TargetUrlDomain }}</a>
{{ else }} {{ else }}
+1 -1
View File
@@ -14,7 +14,7 @@
<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" /> <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> </svg>
{{ end }} {{ end }}
<div class="margin-bottom-widget padding-inline-widget flex flex-column flex-grow"> <div class="margin-bottom-widget padding-inline-widget flex flex-column grow">
<a href="{{ .Link }}" title="{{ .Title }}" class="text-truncate-3-lines color-primary-if-not-visited margin-top-10 margin-bottom-auto" target="_blank" rel="noreferrer">{{ .Title }}</a> <a href="{{ .Link }}" title="{{ .Title }}" class="text-truncate-3-lines color-primary-if-not-visited margin-top-10 margin-bottom-auto" target="_blank" rel="noreferrer">{{ .Title }}</a>
<ul class="list-horizontal-text flex-nowrap margin-top-7"> <ul class="list-horizontal-text flex-nowrap margin-top-7">
<li class="shrink-0" title="{{ .PublishedAt | formatTime }}" {{ dynamicRelativeTimeAttrs .PublishedAt }}>{{ .PublishedAt | relativeTime }}</li> <li class="shrink-0" title="{{ .PublishedAt | formatTime }}" {{ dynamicRelativeTimeAttrs .PublishedAt }}>{{ .PublishedAt | relativeTime }}</li>
+1 -1
View File
@@ -15,7 +15,7 @@
<div class="stock-values shrink-0"> <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="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">${{ .Price | formatPrice }}</div> <div class="text-right">{{ .Currency }}{{ .Price | formatPrice }}</div>
</div> </div>
</li> </li>
{{ end }} {{ end }}
+1 -1
View File
@@ -8,7 +8,7 @@
{{ 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=""> <img class="video-thumbnail thumbnail" loading="lazy" src="{{ .ThumbnailUrl }}" alt="">
<div class="margin-top-10 margin-bottom-widget flex flex-column flex-grow padding-inline-widget"> <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> <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"> <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-0" title="{{ .TimePosted | formatTime }}" {{ dynamicRelativeTimeAttrs .TimePosted }}>{{ .TimePosted | relativeTime }}</li>
+2 -2
View File
@@ -2,7 +2,7 @@
{{ define "widget-content" }} {{ define "widget-content" }}
<div class="size-h2 color-highlight text-center">{{ .Weather.WeatherCodeAsString }}</div> <div class="size-h2 color-highlight text-center">{{ .Weather.WeatherCodeAsString }}</div>
<div class="size-h4 text-center">Feels like {{ .Weather.ApparentTemperature }}°C</div> <div class="size-h4 text-center">Feels like {{ .Weather.ApparentTemperature }}°{{ if eq .Units "metric" }}C{{ else }}F{{ end }}</div>
<div class="weather-columns flex margin-top-15 justify-center"> <div class="weather-columns flex margin-top-15 justify-center">
{{ range $i, $column := .Weather.Columns }} {{ range $i, $column := .Weather.Columns }}
@@ -23,7 +23,7 @@
{{ if not .HideLocation }} {{ if not .HideLocation }}
<div class="flex items-center justify-center margin-top-15 gap-7 size-h5"> <div class="flex items-center justify-center margin-top-15 gap-7 size-h5">
<div class="location-icon"></div> <div class="location-icon"></div>
<div class="text-truncate">{{ .Place.Name }}, {{ .Place.Country }}</div> <div class="text-truncate">{{ .Place.Name }},{{ if .ShowAreaName }} {{ .Place.Area }},{{ end }} {{ .Place.Country }}</div>
</div> </div>
{{ end }} {{ end }}
{{ end }} {{ end }}
+13 -4
View File
@@ -5,6 +5,7 @@ import (
"log/slog" "log/slog"
"net/http" "net/http"
"strconv" "strconv"
"strings"
"time" "time"
) )
@@ -28,7 +29,7 @@ func getHackerNewsTopPostIds() ([]int, error) {
return response, nil return response, nil
} }
func getHackerNewsPostsFromIds(postIds []int) (ForumPosts, error) { func getHackerNewsPostsFromIds(postIds []int, commentsUrlTemplate string) (ForumPosts, error) {
requests := make([]*http.Request, len(postIds)) requests := make([]*http.Request, len(postIds))
for i, id := range postIds { for i, id := range postIds {
@@ -52,9 +53,17 @@ func getHackerNewsPostsFromIds(postIds []int) (ForumPosts, error) {
continue continue
} }
var commentsUrl string
if commentsUrlTemplate == "" {
commentsUrl = "https://news.ycombinator.com/item?id=" + strconv.Itoa(results[i].Id)
} else {
commentsUrl = strings.ReplaceAll(commentsUrlTemplate, "{POST-ID}", strconv.Itoa(results[i].Id))
}
posts = append(posts, ForumPost{ posts = append(posts, ForumPost{
Title: results[i].Title, Title: results[i].Title,
DiscussionUrl: "https://news.ycombinator.com/item?id=" + strconv.Itoa(results[i].Id), DiscussionUrl: commentsUrl,
TargetUrl: results[i].TargetUrl, TargetUrl: results[i].TargetUrl,
TargetUrlDomain: extractDomainFromUrl(results[i].TargetUrl), TargetUrlDomain: extractDomainFromUrl(results[i].TargetUrl),
CommentCount: results[i].CommentCount, CommentCount: results[i].CommentCount,
@@ -74,7 +83,7 @@ func getHackerNewsPostsFromIds(postIds []int) (ForumPosts, error) {
return posts, nil return posts, nil
} }
func FetchHackerNewsTopPosts(limit int) (ForumPosts, error) { func FetchHackerNewsTopPosts(limit int, commentsUrlTemplate string) (ForumPosts, error) {
postIds, err := getHackerNewsTopPostIds() postIds, err := getHackerNewsTopPostIds()
if err != nil { if err != nil {
@@ -85,5 +94,5 @@ func FetchHackerNewsTopPosts(limit int) (ForumPosts, error) {
postIds = postIds[:limit] postIds = postIds[:limit]
} }
return getHackerNewsPostsFromIds(postIds) return getHackerNewsPostsFromIds(postIds, commentsUrlTemplate)
} }
+64 -5
View File
@@ -6,6 +6,7 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"slices" "slices"
"strings"
"time" "time"
_ "time/tzdata" _ "time/tzdata"
@@ -17,6 +18,7 @@ type PlacesResponseJson struct {
type PlaceJson struct { type PlaceJson struct {
Name string Name string
Area string `json:"admin1"`
Latitude float64 Latitude float64
Longitude float64 Longitude float64
Timezone string Timezone string
@@ -48,8 +50,41 @@ type weatherColumn struct {
HasPrecipitation bool HasPrecipitation bool
} }
var commonCountryAbbreviations = map[string]string{
"US": "United States",
"USA": "United States",
"UK": "United Kingdom",
}
func expandCountryAbbreviations(name string) string {
if expanded, ok := commonCountryAbbreviations[strings.TrimSpace(name)]; ok {
return expanded
}
return name
}
// Separates the location that Open Meteo accepts from the administrative area
// which can then be used to filter to the correct place after the list of places
// has been retrieved. Also expands abbreviations since Open Meteo does not accept
// country names like "US", "USA" and "UK"
func parsePlaceName(name string) (string, string) {
parts := strings.Split(name, ",")
if len(parts) == 1 {
return name, ""
}
if len(parts) == 2 {
return parts[0] + ", " + expandCountryAbbreviations(parts[1]), ""
}
return parts[0] + ", " + expandCountryAbbreviations(parts[2]), strings.TrimSpace(parts[1])
}
func FetchPlaceFromName(location string) (*PlaceJson, error) { func FetchPlaceFromName(location string) (*PlaceJson, error) {
requestUrl := fmt.Sprintf("https://geocoding-api.open-meteo.com/v1/search?name=%s&count=1&language=en&format=json", url.QueryEscape(location)) location, area := parsePlaceName(location)
requestUrl := fmt.Sprintf("https://geocoding-api.open-meteo.com/v1/search?name=%s&count=10&language=en&format=json", url.QueryEscape(location))
request, _ := http.NewRequest("GET", requestUrl, nil) request, _ := http.NewRequest("GET", requestUrl, nil)
responseJson, err := decodeJsonFromRequest[PlacesResponseJson](defaultClient, request) responseJson, err := decodeJsonFromRequest[PlacesResponseJson](defaultClient, request)
@@ -61,7 +96,24 @@ func FetchPlaceFromName(location string) (*PlaceJson, error) {
return nil, fmt.Errorf("no places found for %s", location) return nil, fmt.Errorf("no places found for %s", location)
} }
place := &responseJson.Results[0] var place *PlaceJson
if area != "" {
area = strings.ToLower(area)
for i := range responseJson.Results {
if strings.ToLower(responseJson.Results[i].Area) == area {
place = &responseJson.Results[i]
break
}
}
if place == nil {
return nil, fmt.Errorf("no place found for %s in %s", location, area)
}
} else {
place = &responseJson.Results[0]
}
loc, err := time.LoadLocation(place.Timezone) loc, err := time.LoadLocation(place.Timezone)
@@ -79,18 +131,25 @@ func barIndexFromHour(h int) int {
} }
// TODO: bunch of spaget, refactor // TODO: bunch of spaget, refactor
// TODO: allow changing between C and F func FetchWeatherForPlace(place *PlaceJson, units string) (*Weather, error) {
func FetchWeatherForPlace(place *PlaceJson) (*Weather, error) {
query := url.Values{} query := url.Values{}
var temperatureUnit string
if units == "imperial" {
temperatureUnit = "fahrenheit"
} else {
temperatureUnit = "celsius"
}
query.Add("latitude", fmt.Sprintf("%f", place.Latitude)) query.Add("latitude", fmt.Sprintf("%f", place.Latitude))
query.Add("longitude", fmt.Sprintf("%f", place.Longitude)) query.Add("longitude", fmt.Sprintf("%f", place.Longitude))
query.Add("timeformat", "unixtime") query.Add("timeformat", "unixtime")
query.Add("timezone", place.Timezone) query.Add("timezone", place.Timezone)
query.Add("forecast_days", "1") query.Add("forecast_days", "1")
query.Add("current", "temperature_2m,apparent_temperature,weather_code,wind_speed_10m") query.Add("current", "temperature_2m,apparent_temperature,weather_code")
query.Add("hourly", "temperature_2m,precipitation_probability") query.Add("hourly", "temperature_2m,precipitation_probability")
query.Add("daily", "sunrise,sunset") query.Add("daily", "sunrise,sunset")
query.Add("temperature_unit", temperatureUnit)
requestUrl := "https://api.open-meteo.com/v1/forecast?" + query.Encode() requestUrl := "https://api.open-meteo.com/v1/forecast?" + query.Encode()
request, _ := http.NewRequest("GET", requestUrl, nil) request, _ := http.NewRequest("GET", requestUrl, nil)
+26
View File
@@ -59,9 +59,35 @@ type Video struct {
type Videos []Video type Videos []Video
var currencyToSymbol = map[string]string{
"USD": "$",
"EUR": "€",
"JPY": "¥",
"CAD": "C$",
"AUD": "A$",
"GBP": "£",
"CHF": "Fr",
"NZD": "N$",
"INR": "₹",
"BRL": "R$",
"RUB": "₽",
"TRY": "₺",
"ZAR": "R",
"CNY": "¥",
"KRW": "₩",
"HKD": "HK$",
"SGD": "S$",
"SEK": "kr",
"NOK": "kr",
"DKK": "kr",
"PLN": "zł",
"PHP": "₱",
}
type Stock struct { type Stock struct {
Name string Name string
Symbol string Symbol string
Currency string
Price float64 Price float64
PercentChange float64 PercentChange float64
SvgChartPoints string SvgChartPoints string
+19 -3
View File
@@ -5,6 +5,7 @@ import (
"html" "html"
"net/http" "net/http"
"net/url" "net/url"
"strings"
"time" "time"
) )
@@ -12,6 +13,7 @@ type subredditResponseJson struct {
Data struct { Data struct {
Children []struct { Children []struct {
Data struct { Data struct {
Id string `json:"id"`
Title string `json:"title"` Title string `json:"title"`
Upvotes int `json:"ups"` Upvotes int `json:"ups"`
Url string `json:"url"` Url string `json:"url"`
@@ -28,8 +30,12 @@ type subredditResponseJson struct {
} `json:"data"` } `json:"data"`
} }
func FetchSubredditPosts(subreddit string) (ForumPosts, error) { func FetchSubredditPosts(subreddit string, commentsUrlTemplate string, requestUrlTemplate string) (ForumPosts, error) {
requestUrl := fmt.Sprintf("https://www.reddit.com/r/%s/hot.json", url.QueryEscape(subreddit)) subreddit = url.QueryEscape(subreddit)
requestUrl := fmt.Sprintf("https://www.reddit.com/r/%s/hot.json", subreddit)
if requestUrlTemplate != "" {
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 {
@@ -57,9 +63,19 @@ func FetchSubredditPosts(subreddit string) (ForumPosts, error) {
continue continue
} }
var commentsUrl string
if commentsUrlTemplate == "" {
commentsUrl = "https://www.reddit.com" + post.Permalink
} else {
commentsUrl = strings.ReplaceAll(commentsUrlTemplate, "{SUBREDDIT}", subreddit)
commentsUrl = strings.ReplaceAll(commentsUrl, "{POST-ID}", post.Id)
commentsUrl = strings.ReplaceAll(commentsUrl, "{POST-PATH}", strings.TrimLeft(post.Permalink, "/"))
}
forumPost := ForumPost{ forumPost := ForumPost{
Title: html.UnescapeString(post.Title), Title: html.UnescapeString(post.Title),
DiscussionUrl: "https://www.reddit.com" + post.Permalink, DiscussionUrl: commentsUrl,
TargetUrlDomain: post.Domain, TargetUrlDomain: post.Domain,
CommentCount: post.CommentsCount, CommentCount: post.CommentsCount,
Score: post.Upvotes, Score: post.Upvotes,
+22 -2
View File
@@ -23,6 +23,16 @@ func addBrowserUserAgentHeader(request *http.Request) {
request.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:123.0) Gecko/20100101 Firefox/123.0") request.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:123.0) Gecko/20100101 Firefox/123.0")
} }
func truncateString(s string, maxLen int) string {
asRunes := []rune(s)
if len(asRunes) > maxLen {
return string(asRunes[:maxLen])
}
return s
}
func decodeJsonFromRequest[T any](client RequestDoer, request *http.Request) (T, error) { func decodeJsonFromRequest[T any](client RequestDoer, request *http.Request) (T, error) {
response, err := client.Do(request) response, err := client.Do(request)
var result T var result T
@@ -40,7 +50,12 @@ func decodeJsonFromRequest[T any](client RequestDoer, request *http.Request) (T,
} }
if response.StatusCode != http.StatusOK { if response.StatusCode != http.StatusOK {
return result, fmt.Errorf("unexpected status code %d for %s, response: %s", response.StatusCode, request.URL, string(body)) return result, fmt.Errorf(
"unexpected status code %d for %s, response: %s",
response.StatusCode,
request.URL,
truncateString(string(body), 256),
)
} }
err = json.Unmarshal(body, &result) err = json.Unmarshal(body, &result)
@@ -76,7 +91,12 @@ func decodeXmlFromRequest[T any](client RequestDoer, request *http.Request) (T,
} }
if response.StatusCode != http.StatusOK { if response.StatusCode != http.StatusOK {
return result, fmt.Errorf("unexpected status code %d for %s, response: %s", response.StatusCode, request.URL, string(body)) return result, fmt.Errorf(
"unexpected status code %d for %s, response: %s",
response.StatusCode,
request.URL,
truncateString(string(body), 256),
)
} }
err = xml.Unmarshal(body, &result) err = xml.Unmarshal(body, &result)
+8
View File
@@ -10,6 +10,7 @@ type stockResponseJson struct {
Chart struct { Chart struct {
Result []struct { Result []struct {
Meta struct { Meta struct {
Currency string `json:"currency"`
Symbol string `json:"symbol"` Symbol string `json:"symbol"`
RegularMarketPrice float64 `json:"regularMarketPrice"` RegularMarketPrice float64 `json:"regularMarketPrice"`
ChartPreviousClose float64 `json:"chartPreviousClose"` ChartPreviousClose float64 `json:"chartPreviousClose"`
@@ -78,10 +79,17 @@ func FetchStocksDataFromYahoo(stockRequests []StockRequest) (Stocks, error) {
points := SvgPolylineCoordsFromYValues(100, 50, maybeCopySliceWithoutZeroValues(prices)) points := SvgPolylineCoordsFromYValues(100, 50, maybeCopySliceWithoutZeroValues(prices))
currency, exists := currencyToSymbol[response.Chart.Result[0].Meta.Currency]
if !exists {
currency = response.Chart.Result[0].Meta.Currency
}
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, 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,
+17 -2
View File
@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"log/slog" "log/slog"
"net/http" "net/http"
"net/url"
"strings" "strings"
"time" "time"
) )
@@ -38,7 +39,7 @@ func parseYoutubeFeedTime(t string) time.Time {
return parsedTime return parsedTime
} }
func FetchYoutubeChannelUploads(channelIds []string) (Videos, error) { func FetchYoutubeChannelUploads(channelIds []string, videoUrlTemplate string) (Videos, error) {
requests := make([]*http.Request, 0, len(channelIds)) requests := make([]*http.Request, 0, len(channelIds))
for i := range channelIds { for i := range channelIds {
@@ -75,10 +76,24 @@ func FetchYoutubeChannelUploads(channelIds []string) (Videos, error) {
continue continue
} }
var videoUrl string
if videoUrlTemplate == "" {
videoUrl = video.Link.Href
} else {
parsedUrl, err := url.Parse(video.Link.Href)
if err == nil {
videoUrl = strings.ReplaceAll(videoUrlTemplate, "{VIDEO-ID}", parsedUrl.Query().Get("v"))
} else {
videoUrl = "#"
}
}
videos = append(videos, Video{ videos = append(videos, Video{
ThumbnailUrl: video.Group.Thumbnail.Url, ThumbnailUrl: video.Group.Thumbnail.Url,
Title: video.Title, Title: video.Title,
Url: video.Link.Href, Url: videoUrl,
Author: response.Channel, Author: response.Channel,
AuthorUrl: response.ChannelLink.Href + "/videos", AuthorUrl: response.ChannelLink.Href + "/videos",
TimePosted: parseYoutubeFeedTime(video.Published), TimePosted: parseYoutubeFeedTime(video.Published),
+1
View File
@@ -34,6 +34,7 @@ type Theme struct {
Light bool `yaml:"light"` Light bool `yaml:"light"`
ContrastMultiplier float32 `yaml:"contrast-multiplier"` ContrastMultiplier float32 `yaml:"contrast-multiplier"`
TextSaturationMultiplier float32 `yaml:"text-saturation-multiplier"` TextSaturationMultiplier float32 `yaml:"text-saturation-multiplier"`
CustomCSSFile string `yaml:"custom-css-file"`
} }
type Server struct { type Server struct {
+20
View File
@@ -2,6 +2,7 @@ package widget
import ( import (
"html/template" "html/template"
"strings"
"github.com/glanceapp/glance/internal/assets" "github.com/glanceapp/glance/internal/assets"
) )
@@ -15,12 +16,31 @@ type Bookmarks struct {
Links []struct { Links []struct {
Title string `yaml:"title"` Title string `yaml:"title"`
URL string `yaml:"url"` URL string `yaml:"url"`
Icon string `yaml:"icon"`
IsSimpleIcon bool `yaml:"-"`
SameTab bool `yaml:"same-tab"`
HideArrow bool `yaml:"hide-arrow"`
} `yaml:"links"` } `yaml:"links"`
} `yaml:"groups"` } `yaml:"groups"`
} }
func (widget *Bookmarks) Initialize() error { func (widget *Bookmarks) Initialize() error {
widget.withTitle("Bookmarks").withError(nil) widget.withTitle("Bookmarks").withError(nil)
for g := range widget.Groups {
for l := range widget.Groups[g].Links {
if widget.Groups[g].Links[l].Icon == "" {
continue
}
if strings.HasPrefix(widget.Groups[g].Links[l].Icon, "si:") {
icon := strings.TrimPrefix(widget.Groups[g].Links[l].Icon, "si:")
widget.Groups[g].Links[l].IsSimpleIcon = true
widget.Groups[g].Links[l].Icon = "https://cdnjs.cloudflare.com/ajax/libs/simple-icons/11.14.0/" + icon + ".svg"
}
}
}
widget.cachedHTML = widget.render(widget, assets.BookmarksTemplate) widget.cachedHTML = widget.render(widget, assets.BookmarksTemplate)
return nil return nil
+3 -1
View File
@@ -14,6 +14,8 @@ type HackerNews struct {
Posts feed.ForumPosts `yaml:"-"` Posts feed.ForumPosts `yaml:"-"`
Limit int `yaml:"limit"` Limit int `yaml:"limit"`
CollapseAfter int `yaml:"collapse-after"` CollapseAfter int `yaml:"collapse-after"`
CommentsUrlTemplate string `yaml:"comments-url-template"`
ShowThumbnails bool `yaml:"-"`
} }
func (widget *HackerNews) Initialize() error { func (widget *HackerNews) Initialize() error {
@@ -31,7 +33,7 @@ func (widget *HackerNews) Initialize() error {
} }
func (widget *HackerNews) Update(ctx context.Context) { func (widget *HackerNews) Update(ctx context.Context) {
posts, err := feed.FetchHackerNewsTopPosts(40) posts, err := feed.FetchHackerNewsTopPosts(40, widget.CommentsUrlTemplate)
if !widget.canContinueUpdateAfterHandlingErr(err) { if !widget.canContinueUpdateAfterHandlingErr(err) {
return return
+2 -1
View File
@@ -49,6 +49,7 @@ type Monitor struct {
Title string `yaml:"title"` Title string `yaml:"title"`
Url string `yaml:"url"` Url string `yaml:"url"`
IconUrl string `yaml:"icon"` IconUrl string `yaml:"icon"`
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:"-"`
@@ -65,7 +66,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("HEAD", widget.Sites[i].Url, nil) request, err := http.NewRequest("GET", 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)
+11 -1
View File
@@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"html/template" "html/template"
"strings"
"time" "time"
"github.com/glanceapp/glance/internal/assets" "github.com/glanceapp/glance/internal/assets"
@@ -15,8 +16,11 @@ type Reddit struct {
Posts feed.ForumPosts `yaml:"-"` Posts feed.ForumPosts `yaml:"-"`
Subreddit string `yaml:"subreddit"` Subreddit string `yaml:"subreddit"`
Style string `yaml:"style"` Style string `yaml:"style"`
ShowThumbnails bool `yaml:"show-thumbnails"`
CommentsUrlTemplate string `yaml:"comments-url-template"`
Limit int `yaml:"limit"` Limit int `yaml:"limit"`
CollapseAfter int `yaml:"collapse-after"` CollapseAfter int `yaml:"collapse-after"`
RequestUrlTemplate string `yaml:"request-url-template"`
} }
func (widget *Reddit) Initialize() error { func (widget *Reddit) Initialize() error {
@@ -32,13 +36,19 @@ func (widget *Reddit) Initialize() error {
widget.CollapseAfter = 5 widget.CollapseAfter = 5
} }
if widget.RequestUrlTemplate != "" {
if !strings.Contains(widget.RequestUrlTemplate, "{REQUEST-URL}") {
return errors.New("no `{REQUEST-URL}` placeholder specified")
}
}
widget.withTitle("/r/" + widget.Subreddit).withCacheDuration(30 * time.Minute) widget.withTitle("/r/" + widget.Subreddit).withCacheDuration(30 * time.Minute)
return nil return nil
} }
func (widget *Reddit) Update(ctx context.Context) { func (widget *Reddit) Update(ctx context.Context) {
posts, err := feed.FetchSubredditPosts(widget.Subreddit) posts, err := feed.FetchSubredditPosts(widget.Subreddit, widget.CommentsUrlTemplate, widget.RequestUrlTemplate)
if !widget.canContinueUpdateAfterHandlingErr(err) { if !widget.canContinueUpdateAfterHandlingErr(err) {
return return
+4
View File
@@ -12,6 +12,7 @@ import (
type Stocks struct { type Stocks struct {
widgetBase `yaml:",inline"` widgetBase `yaml:",inline"`
Stocks feed.Stocks `yaml:"-"` Stocks feed.Stocks `yaml:"-"`
Sort string `yaml:"sort-by"`
Tickers []feed.StockRequest `yaml:"stocks"` Tickers []feed.StockRequest `yaml:"stocks"`
} }
@@ -28,7 +29,10 @@ func (widget *Stocks) Update(ctx context.Context) {
return return
} }
if widget.Sort == "absolute-change" {
stocks.SortByAbsChange() stocks.SortByAbsChange()
}
widget.Stocks = stocks widget.Stocks = stocks
} }
+2 -1
View File
@@ -12,6 +12,7 @@ import (
type Videos struct { type Videos struct {
widgetBase `yaml:",inline"` widgetBase `yaml:",inline"`
Videos feed.Videos `yaml:"-"` Videos feed.Videos `yaml:"-"`
VideoUrlTemplate string `yaml:"video-url-template"`
Channels []string `yaml:"channels"` Channels []string `yaml:"channels"`
Limit int `yaml:"limit"` Limit int `yaml:"limit"`
} }
@@ -27,7 +28,7 @@ func (widget *Videos) Initialize() error {
} }
func (widget *Videos) Update(ctx context.Context) { func (widget *Videos) Update(ctx context.Context) {
videos, err := feed.FetchYoutubeChannelUploads(widget.Channels) videos, err := feed.FetchYoutubeChannelUploads(widget.Channels, widget.VideoUrlTemplate)
if !widget.canContinueUpdateAfterHandlingErr(err) { if !widget.canContinueUpdateAfterHandlingErr(err) {
return return
+9 -1
View File
@@ -12,7 +12,9 @@ import (
type Weather struct { type Weather struct {
widgetBase `yaml:",inline"` widgetBase `yaml:",inline"`
Location string `yaml:"location"` Location string `yaml:"location"`
ShowAreaName bool `yaml:"show-area-name"`
HideLocation bool `yaml:"hide-location"` HideLocation bool `yaml:"hide-location"`
Units string `yaml:"units"`
Place *feed.PlaceJson `yaml:"-"` Place *feed.PlaceJson `yaml:"-"`
Weather *feed.Weather `yaml:"-"` Weather *feed.Weather `yaml:"-"`
TimeLabels [12]string `yaml:"-"` TimeLabels [12]string `yaml:"-"`
@@ -24,6 +26,12 @@ func (widget *Weather) Initialize() error {
widget.withTitle("Weather").withCacheOnTheHour() widget.withTitle("Weather").withCacheOnTheHour()
widget.TimeLabels = timeLabels widget.TimeLabels = timeLabels
if widget.Units == "" {
widget.Units = "metric"
} else if widget.Units != "metric" && widget.Units != "imperial" {
return fmt.Errorf("invalid units '%s' for weather, must be either metric or imperial", widget.Units)
}
place, err := feed.FetchPlaceFromName(widget.Location) place, err := feed.FetchPlaceFromName(widget.Location)
if err != nil { if err != nil {
@@ -36,7 +44,7 @@ func (widget *Weather) Initialize() error {
} }
func (widget *Weather) Update(ctx context.Context) { func (widget *Weather) Update(ctx context.Context) {
weather, err := feed.FetchWeatherForPlace(widget.Place) weather, err := feed.FetchWeatherForPlace(widget.Place, widget.Units)
if !widget.canContinueUpdateAfterHandlingErr(err) { if !widget.canContinueUpdateAfterHandlingErr(err) {
return return