Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b444b88e3 | ||
|
|
f317b1b189 | ||
|
|
3523562c3a | ||
|
|
5fdc3e3f10 | ||
|
|
24ef8f6e2a | ||
|
|
d8d6625478 | ||
|
|
ad06146784 | ||
|
|
c97171883c | ||
|
|
9c5b6045af | ||
|
|
eb85a7656f | ||
|
|
25d45b309d | ||
|
|
7d0b4535bb | ||
|
|
f8af2025e8 | ||
|
|
948c117f5b | ||
|
|
58967ab758 | ||
|
|
44a153d30a | ||
|
|
d5d6103327 | ||
|
|
189b8895b0 | ||
|
|
748ad1ff8d | ||
|
|
ccdafcdfab | ||
|
|
78572a534b | ||
|
|
2eb8e71e89 | ||
|
|
5375103c4b | ||
|
|
faeeb7c899 | ||
|
|
99fd77feac |
+163
-12
@@ -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
|
||||||

|

|
||||||
@@ -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`
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
##### `show-thumbnails`
|
||||||
|
Shows or hides thumbnails next to the post. This only works if the `style` is `vertical-list`. Preview:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
> [!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:
|
||||||
|
|
||||||

|

|
||||||
@@ -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 |
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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=
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
@@ -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,15 +4,32 @@
|
|||||||
<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 }}>
|
||||||
<a href="{{ $post.DiscussionUrl }}" class="size-h3 color-primary-if-not-visited" target="_blank" rel="noreferrer">{{ .Title }}</a>
|
<div class="forum-post-list-item thumbnail-container">
|
||||||
<ul class="list-horizontal-text">
|
{{ if $.ShowThumbnails }}
|
||||||
<li title="{{ $post.TimePosted | formatTime }}" {{ dynamicRelativeTimeAttrs $post.TimePosted }}>{{ $post.TimePosted | relativeTime }}</li>
|
{{ if ne $post.ThumbnailUrl "" }}
|
||||||
<li>{{ $post.Score | formatNumber }} points</li>
|
<img class="forum-post-list-thumbnail thumbnail" src="{{ $post.ThumbnailUrl }}" alt="" loading="lazy">
|
||||||
<li>{{ $post.CommentCount | formatNumber }} comments</li>
|
{{ else if $post.HasTargetUrl }}
|
||||||
{{ 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)">
|
||||||
<li class="shrink min-width-0"><a class="visited-indicator text-truncate block" href="{{ .TargetUrl }}" target="_blank" rel="noreferrer">{{ $post.TargetUrlDomain }}</a></li>
|
<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 }}
|
{{ end }}
|
||||||
</ul>
|
<div class="grow">
|
||||||
|
<a href="{{ $post.DiscussionUrl }}" class="size-h3 color-primary-if-not-visited" target="_blank" rel="noreferrer">{{ .Title }}</a>
|
||||||
|
<ul class="list-horizontal-text">
|
||||||
|
<li title="{{ $post.TimePosted | formatTime }}" {{ dynamicRelativeTimeAttrs $post.TimePosted }}>{{ $post.TimePosted | relativeTime }}</li>
|
||||||
|
<li>{{ $post.Score | formatNumber }} points</li>
|
||||||
|
<li>{{ $post.CommentCount | formatNumber }} comments</li>
|
||||||
|
{{ if $post.HasTargetUrl }}
|
||||||
|
<li class="shrink min-width-0"><a class="visited-indicator text-truncate block" href="{{ .TargetUrl }}" target="_blank" rel="noreferrer">{{ $post.TargetUrlDomain }}</a></li>
|
||||||
|
{{ end }}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 }}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 }}
|
||||||
|
|||||||
@@ -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,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 }}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
+11
-3
@@ -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,
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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"
|
||||||
)
|
)
|
||||||
@@ -13,14 +14,33 @@ type Bookmarks struct {
|
|||||||
Title string `yaml:"title"`
|
Title string `yaml:"title"`
|
||||||
Color *HSLColorField `yaml:"color"`
|
Color *HSLColorField `yaml:"color"`
|
||||||
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
|
||||||
|
|||||||
@@ -10,10 +10,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type HackerNews struct {
|
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"`
|
||||||
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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -11,12 +12,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Reddit struct {
|
type Reddit struct {
|
||||||
widgetBase `yaml:",inline"`
|
widgetBase `yaml:",inline"`
|
||||||
Posts feed.ForumPosts `yaml:"-"`
|
Posts feed.ForumPosts `yaml:"-"`
|
||||||
Subreddit string `yaml:"subreddit"`
|
Subreddit string `yaml:"subreddit"`
|
||||||
Style string `yaml:"style"`
|
Style string `yaml:"style"`
|
||||||
Limit int `yaml:"limit"`
|
ShowThumbnails bool `yaml:"show-thumbnails"`
|
||||||
CollapseAfter int `yaml:"collapse-after"`
|
CommentsUrlTemplate string `yaml:"comments-url-template"`
|
||||||
|
Limit int `yaml:"limit"`
|
||||||
|
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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
stocks.SortByAbsChange()
|
if widget.Sort == "absolute-change" {
|
||||||
|
stocks.SortByAbsChange()
|
||||||
|
}
|
||||||
|
|
||||||
widget.Stocks = stocks
|
widget.Stocks = stocks
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,10 +10,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Videos struct {
|
type Videos struct {
|
||||||
widgetBase `yaml:",inline"`
|
widgetBase `yaml:",inline"`
|
||||||
Videos feed.Videos `yaml:"-"`
|
Videos feed.Videos `yaml:"-"`
|
||||||
Channels []string `yaml:"channels"`
|
VideoUrlTemplate string `yaml:"video-url-template"`
|
||||||
Limit int `yaml:"limit"`
|
Channels []string `yaml:"channels"`
|
||||||
|
Limit int `yaml:"limit"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (widget *Videos) Initialize() error {
|
func (widget *Videos) Initialize() error {
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user