mirror of
https://github.com/glanceapp/glance.git
synced 2024-09-11 04:55:38 +03:00
commit
7743664527
@ -16,6 +16,7 @@
|
||||
* iframe
|
||||
* Twitch channels & top games
|
||||
* GitHub releases
|
||||
* Repository overview
|
||||
* Site monitor
|
||||
|
||||
#### Themeable
|
||||
@ -48,7 +49,7 @@ Checkout the [releases page](https://github.com/glanceapp/glance/releases) for a
|
||||
#### Docker
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> Make sure you have a valid `glance.yml` file before running the container.
|
||||
> Make sure you have a valid `glance.yml` file in the same directory before running the container.
|
||||
|
||||
```bash
|
||||
docker run -d -p 8080:8080 \
|
||||
|
@ -14,6 +14,7 @@
|
||||
- [Weather](#weather)
|
||||
- [Monitor](#monitor)
|
||||
- [Releases](#releases)
|
||||
- [Repository](#repository)
|
||||
- [Bookmarks](#bookmarks)
|
||||
- [Calendar](#calendar)
|
||||
- [Stocks](#stocks)
|
||||
@ -250,11 +251,12 @@ pages:
|
||||
```
|
||||
|
||||
### Properties
|
||||
| Name | Type | Required |
|
||||
| ---- | ---- | -------- |
|
||||
| title | string | yes |
|
||||
| slug | string | no |
|
||||
| columns | array | yes |
|
||||
| Name | Type | Required | Default |
|
||||
| ---- | ---- | -------- | ------- |
|
||||
| title | string | yes | |
|
||||
| slug | string | no | |
|
||||
| show-mobile-header | boolean | no | false |
|
||||
| columns | array | yes | |
|
||||
|
||||
#### `title`
|
||||
The name of the page which gets shown in the navigation bar.
|
||||
@ -262,6 +264,13 @@ The name of the page which gets shown in the navigation bar.
|
||||
#### `slug`
|
||||
The URL friendly version of the title which is used to access the page. For example if the title of the page is "RSS Feeds" you can make the page accessible via `localhost:8080/feeds` by setting the slug to `feeds`. If not defined, it will automatically be generated from the title.
|
||||
|
||||
#### `show-mobile-header`
|
||||
Whether to show a header displaying the name of the page on mobile. The header purposefully has a lot of vertical whitespace in order to push the content down and make it easier to reach on tall devices.
|
||||
|
||||
Preview:
|
||||
|
||||
![](images/mobile-header-preview.png)
|
||||
|
||||
### Columns
|
||||
Columns are defined for each page using a `columns` property. There are two types of columns - `full` and `small`, which refers to their width. A small column takes up a fixed amount of width (300px) and a full column takes up the all of the remaining width. You can have up to 3 columns per page and you must have either 1 or 2 full columns. Example:
|
||||
|
||||
@ -384,6 +393,8 @@ Example:
|
||||
| ---- | ---- | -------- | ------- |
|
||||
| style | string | no | vertical-list |
|
||||
| feeds | array | yes |
|
||||
| thumbnail-height | float | no | 10 |
|
||||
| card-height | float | no | 27 |
|
||||
| limit | integer | no | 25 |
|
||||
| collapse-after | integer | no | 5 |
|
||||
|
||||
@ -398,6 +409,16 @@ Used to change the appearance of the widget. Possible values are `vertical-list`
|
||||
|
||||
![preview of horizontal-cards style for RSS widget](images/rss-feed-horizontal-cards-preview.png)
|
||||
|
||||
`horizontal-cards-2`
|
||||
|
||||
![preview of horizontal-cards-2 style for RSS widget](images/rss-widget-horizontal-cards-2-preview.png)
|
||||
|
||||
##### `thumbnail-height`
|
||||
Used to modify the height of the thumbnails. Works only when the style is set to `horizontal-cards`. The default value is `10` and the units are `rem`, if you want to for example double the height of the thumbnails you can set it to `20`.
|
||||
|
||||
##### `card-height`
|
||||
Used to modify the height of cards when using the `horizontal-cards-2` style. The default value is `27` and the units are `rem`.
|
||||
|
||||
##### `feeds`
|
||||
An array of RSS/atom feeds. The title can optionally be changed.
|
||||
|
||||
@ -434,6 +455,7 @@ Preview:
|
||||
| ---- | ---- | -------- | ------- |
|
||||
| channels | array | yes | |
|
||||
| limit | integer | no | 25 |
|
||||
| style | string | no | horizontal-cards |
|
||||
| video-url-template | string | no | https://www.youtube.com/watch?v={VIDEO-ID} |
|
||||
|
||||
##### `channels`
|
||||
@ -448,6 +470,13 @@ Then scroll down and click on "Share channel", then "Copy channel ID":
|
||||
##### `limit`
|
||||
The maximum number of videos to show.
|
||||
|
||||
##### `style`
|
||||
Used to change the appearance of the widget. Possible values are `horizontal-cards` and `grid-cards`.
|
||||
|
||||
Preview of `grid-cards`:
|
||||
|
||||
![](images/videos-widget-grid-cards-preview.png)
|
||||
|
||||
##### `video-url-template`
|
||||
Used to replace the default link for videos. Useful when you're running your own YouTube front-end. Example:
|
||||
|
||||
@ -479,6 +508,8 @@ Preview:
|
||||
| limit | integer | no | 15 |
|
||||
| collapse-after | integer | no | 5 |
|
||||
| comments-url-template | string | no | https://news.ycombinator.com/item?id={POST-ID} |
|
||||
| sort-by | string | no | top |
|
||||
| extra-sort-by | string | no | |
|
||||
|
||||
##### `comments-url-template`
|
||||
Used to replace the default link for post comments. Useful if you want to use an alternative front-end. Example:
|
||||
@ -491,12 +522,20 @@ Placeholders:
|
||||
|
||||
`{POST-ID}` - the ID of the post
|
||||
|
||||
##### `sort-by`
|
||||
Used to specify the order in which the posts should get returned. Possible values are `top`, `new`, and `best`.
|
||||
|
||||
##### `extra-sort-by`
|
||||
Can be used to specify an additional sort which will be applied on top of the already sorted posts. By default does not apply any extra sorting and the only available option is `engagement`.
|
||||
|
||||
The `engagement` sort tries to place the posts with the most points and comments on top, also prioritizing recent over old posts.
|
||||
|
||||
### Reddit
|
||||
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.
|
||||
> Reddit does not allow unauthorized API access from VPS IPs, if you're hosting Glance on a VPS you will get a 403 response. As a workaround you can route the traffic from Glance through a VPN or your own HTTP proxy using the `request-url-template` property.
|
||||
|
||||
Example:
|
||||
|
||||
@ -515,6 +554,10 @@ Example:
|
||||
| collapse-after | integer | no | 5 |
|
||||
| comments-url-template | string | no | https://www.reddit.com/{POST-PATH} |
|
||||
| request-url-template | string | no | |
|
||||
| sort-by | string | no | hot |
|
||||
| top-period | string | no | day |
|
||||
| search | string | no | |
|
||||
| extra-sort-by | string | no | |
|
||||
|
||||
##### `subreddit`
|
||||
The subreddit for which to fetch the posts from.
|
||||
@ -580,6 +623,22 @@ https://proxy/{REQUEST-URL}
|
||||
https://your.proxy/?url={REQUEST-URL}
|
||||
```
|
||||
|
||||
##### `sort-by`
|
||||
Can be used to specify the order in which the posts should get returned. Possible values are `hot`, `new`, `top` and `rising`.
|
||||
|
||||
##### `top-perid`
|
||||
Available only when `sort-by` is set to `top`. Possible values are `hour`, `day`, `week`, `month`, `year` and `all`.
|
||||
|
||||
##### `search`
|
||||
Keywords to search for. Searching within specific fields is also possible, **though keep in mind that Reddit may remove the ability to use any of these at any time**:
|
||||
|
||||
![](images/reddit-field-search.png)
|
||||
|
||||
##### `extra-sort-by`
|
||||
Can be used to specify an additional sort which will be applied on top of the already sorted posts. By default does not apply any extra sorting and the only available option is `engagement`.
|
||||
|
||||
The `engagement` sort tries to place the posts with the most points and comments on top, also prioritizing recent over old posts.
|
||||
|
||||
### Weather
|
||||
Display weather information for a specific location. The data is provided by https://open-meteo.com/.
|
||||
|
||||
@ -593,7 +652,7 @@ Example:
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> US cities which have common names can have their state specified as the second parameter like such:
|
||||
> US cities which have common names can have their state specified as the second parameter as such:
|
||||
>
|
||||
> * Greenville, North Carolina, United States
|
||||
> * Greenville, South Carolina, United States
|
||||
@ -675,7 +734,11 @@ You can hover over the "ERROR" text to view more information.
|
||||
|
||||
| Name | Type | Required |
|
||||
| ---- | ---- | -------- |
|
||||
| sites | array | yes | |
|
||||
| sites | array | yes |
|
||||
| style | string | no |
|
||||
|
||||
##### `style`
|
||||
To make the widget scale appropriately in a `full` size column, set the style to the experimental `dynamic-columns-experimental` option.
|
||||
|
||||
##### `sites`
|
||||
|
||||
@ -694,7 +757,7 @@ The title used to indicate the site.
|
||||
|
||||
`url`
|
||||
|
||||
The URL which will be requested and its response will determine the status of the site.
|
||||
The URL which will be requested and its response will determine the status of the site. Optionally, you can specify this using an environment variable with the syntax `${VARIABLE_NAME}`.
|
||||
|
||||
`icon`
|
||||
|
||||
@ -763,6 +826,43 @@ The maximum number of releases to show.
|
||||
#### `collapse-after`
|
||||
How many releases are visible before the "SHOW MORE" button appears. Set to `-1` to never collapse.
|
||||
|
||||
### Repository
|
||||
Display general information about a repository as well as a list of the latest open pull requests and issues.
|
||||
|
||||
Example:
|
||||
|
||||
```yaml
|
||||
- type: repository
|
||||
repository: glanceapp/glance
|
||||
pull-requests-limit: 5
|
||||
issues-limit: 3
|
||||
```
|
||||
|
||||
Preview:
|
||||
|
||||
![](images/repository-preview.png)
|
||||
|
||||
#### Properties
|
||||
|
||||
| Name | Type | Required | Default |
|
||||
| ---- | ---- | -------- | ------- |
|
||||
| repository | string | yes | |
|
||||
| token | string | no | |
|
||||
| pull-requests-limit | integer | no | 3 |
|
||||
| issues-limit | integer | no | 3 |
|
||||
|
||||
##### `repository`
|
||||
The owner and repository name that will have their information displayed.
|
||||
|
||||
##### `token`
|
||||
Without authentication Github allows for up to 60 requests per hour. You can easily exceed this limit and start seeing errors if your cache time is low or you have many instances of this widget. To circumvent this you can [create a read only token from your Github account](https://github.com/settings/personal-access-tokens/new) and provide it here.
|
||||
|
||||
##### `pull-requests-limit`
|
||||
The maximum number of latest open pull requests to show. Set to `-1` to not show any.
|
||||
|
||||
##### `issues-limit`
|
||||
The maximum number of latest open issues to show. Set to `-1` to not show any.
|
||||
|
||||
### Bookmarks
|
||||
Display a list of links which can be grouped.
|
||||
|
||||
@ -812,10 +912,14 @@ Preview:
|
||||
| Name | Type | Required |
|
||||
| ---- | ---- | -------- |
|
||||
| groups | array | yes |
|
||||
| style | string | no |
|
||||
|
||||
##### `groups`
|
||||
An array of groups which can optionally have a title and a custom color.
|
||||
|
||||
##### `style`
|
||||
To make the widget scale appropriately in a `full` size column, set the style to the experimental `dynamic-columns-experimental` option.
|
||||
|
||||
###### Properties for each group
|
||||
| Name | Type | Required | Default |
|
||||
| ---- | ---- | -------- | ------- |
|
||||
@ -883,18 +987,12 @@ Example:
|
||||
name: S&P 500
|
||||
- symbol: BTC-USD
|
||||
name: Bitcoin
|
||||
chart-link: https://www.tradingview.com/chart/?symbol=INDEX:BTCUSD
|
||||
- symbol: NVDA
|
||||
name: NVIDIA
|
||||
- symbol: AAPL
|
||||
symbol-link: https://www.google.com/search?tbm=nws&q=apple
|
||||
name: Apple
|
||||
- symbol: MSFT
|
||||
name: Microsoft
|
||||
- symbol: GOOGL
|
||||
name: Google
|
||||
- symbol: AMD
|
||||
name: AMD
|
||||
- symbol: RDDT
|
||||
name: Reddit
|
||||
```
|
||||
|
||||
Preview:
|
||||
@ -907,15 +1005,24 @@ Preview:
|
||||
| ---- | ---- | -------- |
|
||||
| stocks | array | yes |
|
||||
| sort-by | string | no |
|
||||
| style | string | no |
|
||||
|
||||
##### `stocks`
|
||||
An array of stocks for which to display information about.
|
||||
|
||||
##### `sort-by`
|
||||
By default the stocks are displayed in the order they were defined. You can customize their ordering by setting the `sort-by` property to `absolute-change` for descending order based on the stock's absolute price change.
|
||||
|
||||
##### `style`
|
||||
To make the widget scale appropriately in a `full` size column, set the style to the experimental `dynamic-columns-experimental` option.
|
||||
|
||||
###### Properties for each stock
|
||||
| Name | Type | Required |
|
||||
| ---- | ---- | -------- |
|
||||
| symbol | string | yes |
|
||||
| name | string | no |
|
||||
| symbol-link | string | no |
|
||||
| chart-link | string | no |
|
||||
|
||||
`symbol`
|
||||
|
||||
@ -925,8 +1032,11 @@ The symbol, as seen in Yahoo Finance.
|
||||
|
||||
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.
|
||||
`symbol-link`
|
||||
The link to go to when clicking on the symbol.
|
||||
|
||||
`chart-link`
|
||||
The link to go to when clicking on the chart.
|
||||
|
||||
### Twitch Channels
|
||||
Display a list of channels from Twitch.
|
||||
|
BIN
docs/images/mobile-header-preview.png
Normal file
BIN
docs/images/mobile-header-preview.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 128 KiB |
BIN
docs/images/reddit-field-search.png
Normal file
BIN
docs/images/reddit-field-search.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 44 KiB |
BIN
docs/images/repository-preview.png
Normal file
BIN
docs/images/repository-preview.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
BIN
docs/images/rss-widget-horizontal-cards-2-preview.png
Normal file
BIN
docs/images/rss-widget-horizontal-cards-2-preview.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 531 KiB |
BIN
docs/images/videos-widget-grid-cards-preview.png
Normal file
BIN
docs/images/videos-widget-grid-cards-preview.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 330 KiB |
@ -102,12 +102,13 @@
|
||||
.list { --list-half-gap: 0rem; }
|
||||
.list-gap-2 { --list-half-gap: 0.1rem; }
|
||||
.list-gap-4 { --list-half-gap: 0.2rem; }
|
||||
.list-gap-10 { --list-half-gap: 0.5rem; }
|
||||
.list-gap-14 { --list-half-gap: 0.7rem; }
|
||||
.list-gap-20 { --list-half-gap: 1rem; }
|
||||
.list-gap-24 { --list-half-gap: 1.2rem; }
|
||||
|
||||
.list > *:not(:first-child) {
|
||||
margin-top: calc(var(--list-half-gap) * 2 + 1px);
|
||||
margin-top: calc(var(--list-half-gap) * 2);
|
||||
}
|
||||
|
||||
.list-with-separator > *:not(:first-child) {
|
||||
@ -205,11 +206,29 @@
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 0;
|
||||
height: 1px;
|
||||
background-color: var(--color-separator);
|
||||
}
|
||||
|
||||
img, svg {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
img[loading=lazy].loaded:not(.finished-transition) {
|
||||
transition: opacity .4s;
|
||||
}
|
||||
|
||||
img[loading=lazy].cached:not(.finished-transition) {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
img[loading=lazy]:not(.loaded, .cached) {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
scrollbar-color: var(--color-text-subdue) transparent;
|
||||
scroll-behavior: smooth;
|
||||
@ -314,6 +333,44 @@ body {
|
||||
padding: 0 var(--content-bounds-padding);
|
||||
}
|
||||
|
||||
.dynamic-columns {
|
||||
gap: calc(var(--widget-content-vertical-padding) / 2);
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--columns-per-row), 1fr);
|
||||
margin: calc(0px - var(--widget-content-vertical-padding) / 2) calc(0px - var(--widget-content-horizontal-padding) / 2);
|
||||
}
|
||||
|
||||
.dynamic-columns > * {
|
||||
padding: calc(var(--widget-content-vertical-padding) / 2) calc(var(--widget-content-horizontal-padding) / 1.5);
|
||||
background-color: var(--color-background);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.dynamic-columns:has(> :nth-child(1)) { --columns-per-row: 1; }
|
||||
.dynamic-columns:has(> :nth-child(2)) { --columns-per-row: 2; }
|
||||
.dynamic-columns:has(> :nth-child(3)) { --columns-per-row: 3; }
|
||||
.dynamic-columns:has(> :nth-child(4)) { --columns-per-row: 4; }
|
||||
.dynamic-columns:has(> :nth-child(5)) { --columns-per-row: 5; }
|
||||
|
||||
@container widget (max-width: 1500px) {
|
||||
.dynamic-columns:has(> :nth-child(1)) { --columns-per-row: 1; }
|
||||
.dynamic-columns:has(> :nth-child(2)) { --columns-per-row: 2; }
|
||||
.dynamic-columns:has(> :nth-child(3)) { --columns-per-row: 3; }
|
||||
.dynamic-columns:has(> :nth-child(4)) { --columns-per-row: 4; }
|
||||
}
|
||||
@container widget (max-width: 1250px) {
|
||||
.dynamic-columns:has(> :nth-child(1)) { --columns-per-row: 1; }
|
||||
.dynamic-columns:has(> :nth-child(2)) { --columns-per-row: 2; }
|
||||
.dynamic-columns:has(> :nth-child(3)) { --columns-per-row: 3; }
|
||||
}
|
||||
@container widget (max-width: 850px) {
|
||||
.dynamic-columns:has(> :nth-child(1)) { --columns-per-row: 1; }
|
||||
.dynamic-columns:has(> :nth-child(2)) { --columns-per-row: 2; }
|
||||
}
|
||||
@container widget (max-width: 550px) {
|
||||
.dynamic-columns:has(> :nth-child(1)) { --columns-per-row: 1; }
|
||||
}
|
||||
|
||||
.cards-vertical {
|
||||
flex-direction: column;
|
||||
}
|
||||
@ -322,30 +379,44 @@ body {
|
||||
--cards-per-row: 6.5;
|
||||
}
|
||||
|
||||
.cards-grid {
|
||||
--cards-per-row: 6;
|
||||
}
|
||||
|
||||
.cards-horizontal, .cards-vertical, .cards-grid {
|
||||
.cards-horizontal, .cards-vertical {
|
||||
--cards-gap: calc(var(--widget-content-vertical-padding) * 0.7);
|
||||
display: flex;
|
||||
gap: var(--cards-gap);
|
||||
}
|
||||
|
||||
.card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.cards-horizontal .card {
|
||||
flex-shrink: 0;
|
||||
width: calc(100% / var(--cards-per-row) - var(--cards-gap) * (var(--cards-per-row) - 1) / var(--cards-per-row));
|
||||
}
|
||||
|
||||
.cards-grid .card {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.cards-horizontal {
|
||||
overflow-x: auto;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.cards-grid {
|
||||
flex-wrap: wrap;
|
||||
--cards-per-row: 6;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--cards-per-row), 1fr);
|
||||
gap: calc(var(--widget-content-vertical-padding) * 0.7);
|
||||
}
|
||||
|
||||
@container widget (max-width: 1300px) { .cards-horizontal { --cards-per-row: 5.5; } }
|
||||
@container widget (max-width: 1100px) { .cards-horizontal { --cards-per-row: 4.5; } }
|
||||
@container widget (max-width: 850px) { .cards-horizontal { --cards-per-row: 3.5; } }
|
||||
@container widget (max-width: 750px) { .cards-horizontal { --cards-per-row: 3.5; } }
|
||||
@container widget (max-width: 650px) { .cards-horizontal { --cards-per-row: 2.2; } }
|
||||
@container widget (max-width: 650px) { .cards-horizontal { --cards-per-row: 2.5; } }
|
||||
@container widget (max-width: 450px) { .cards-horizontal { --cards-per-row: 2.3; } }
|
||||
|
||||
@container widget (max-width: 1300px) { .cards-grid { --cards-per-row: 5; } }
|
||||
@container widget (max-width: 1100px) { .cards-grid { --cards-per-row: 4; } }
|
||||
@ -353,12 +424,7 @@ body {
|
||||
@container widget (max-width: 750px) { .cards-grid { --cards-per-row: 3; } }
|
||||
@container widget (max-width: 650px) { .cards-grid { --cards-per-row: 2; } }
|
||||
|
||||
.card {
|
||||
flex-shrink: 0;
|
||||
width: calc(100% / var(--cards-per-row) - var(--cards-gap) * (var(--cards-per-row) - 1) / var(--cards-per-row));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
|
||||
.widget-error-header {
|
||||
display: flex;
|
||||
@ -490,7 +556,7 @@ body {
|
||||
animation-delay: 150ms;
|
||||
}
|
||||
|
||||
.mobile-navigation {
|
||||
.mobile-navigation, .mobile-reachability-header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@ -517,6 +583,10 @@ body {
|
||||
width: 6.5rem;
|
||||
}
|
||||
|
||||
.stock-chart svg {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.stock-values {
|
||||
min-width: 8rem;
|
||||
}
|
||||
@ -553,7 +623,7 @@ body {
|
||||
|
||||
.video-thumbnail {
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
aspect-ratio: 16 / 8.9;
|
||||
object-fit: cover;
|
||||
border-radius: var(--border-radius) var(--border-radius) 0 0;
|
||||
}
|
||||
@ -788,6 +858,7 @@ body {
|
||||
}
|
||||
|
||||
.monitor-site-status-icon {
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
@ -805,11 +876,48 @@ body {
|
||||
}
|
||||
|
||||
.rss-card-image {
|
||||
height: 10rem;
|
||||
height: var(--rss-thumbnail-height, 10rem);
|
||||
object-fit: cover;
|
||||
border-radius: var(--border-radius) var(--border-radius) 0 0;
|
||||
}
|
||||
|
||||
.rss-card-2 {
|
||||
position: relative;
|
||||
height: var(--rss-card-height, 27rem);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.rss-card-2::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
background-image: linear-gradient(
|
||||
0deg,
|
||||
var(--color-widget-background),
|
||||
hsla(var(--color-widget-background-hsl-values), 0.8) 6rem, transparent 14rem
|
||||
);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.rss-card-2-image {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
/* +1px is required to fix some weird graphical bug where the image overflows on the bottom in firefox */
|
||||
border-radius: calc(var(--border-radius) + 1px);
|
||||
opacity: 0.9;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.rss-card-2-content {
|
||||
position: absolute;
|
||||
inset-inline: 0;
|
||||
bottom: var(--widget-content-vertical-padding);
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.twitch-category-thumbnail {
|
||||
width: 5rem;
|
||||
border-radius: var(--border-radius);
|
||||
@ -1013,6 +1121,8 @@ body {
|
||||
--content-bounds-padding: 10px;
|
||||
}
|
||||
|
||||
.dynamic-columns:has(> :nth-child(1)) { --columns-per-row: 1; }
|
||||
|
||||
.forum-post-list-item {
|
||||
flex-flow: row-reverse;
|
||||
}
|
||||
@ -1020,6 +1130,15 @@ body {
|
||||
.hide-on-mobile {
|
||||
display: none
|
||||
}
|
||||
|
||||
.mobile-reachability-header {
|
||||
display: block;
|
||||
font-size: 3rem;
|
||||
padding: 10dvh 1rem;
|
||||
text-align: center;
|
||||
color: var(--color-text-highlight);
|
||||
animation: pageColumnsEntrance .3s cubic-bezier(0.25, 1, 0.5, 1) backwards;
|
||||
}
|
||||
}
|
||||
|
||||
.size-h1 { font-size: var(--font-size-h1); }
|
||||
@ -1041,6 +1160,8 @@ body {
|
||||
.text-left { text-align: left; }
|
||||
.text-right { text-align: right; }
|
||||
.text-center { text-align: center; }
|
||||
.text-elevate { margin-top: -0.2em; }
|
||||
.text-compact { word-spacing: -0.18em; }
|
||||
.rtl { direction: rtl; }
|
||||
.shrink { flex-shrink: 1; }
|
||||
.shrink-0 { flex-shrink: 0; }
|
||||
@ -1069,6 +1190,11 @@ body {
|
||||
.margin-top-7 { margin-top: 0.7rem; }
|
||||
.margin-top-10 { margin-top: 1rem; }
|
||||
.margin-top-15 { margin-top: 1.5rem; }
|
||||
.margin-block-3 { margin-block: 0.3rem; }
|
||||
.margin-block-5 { margin-block: 0.5rem; }
|
||||
.margin-block-7 { margin-block: 0.7rem; }
|
||||
.margin-block-10 { margin-block: 1rem; }
|
||||
.margin-block-15 { margin-block: 1.5rem; }
|
||||
.margin-bottom-3 { margin-bottom: 0.3rem; }
|
||||
.margin-bottom-5 { margin-bottom: 0.5rem; }
|
||||
.margin-bottom-7 { margin-bottom: 0.7rem; }
|
||||
|
@ -142,6 +142,33 @@ function setupDynamicRelativeTime() {
|
||||
});
|
||||
}
|
||||
|
||||
function setupLazyImages() {
|
||||
const images = document.querySelectorAll("img[loading=lazy]");
|
||||
|
||||
if (images.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
function imageFinishedTransition(image) {
|
||||
image.classList.add("finished-transition");
|
||||
}
|
||||
|
||||
for (let i = 0; i < images.length; i++) {
|
||||
const image = images[i];
|
||||
|
||||
if (image.complete) {
|
||||
image.classList.add("cached");
|
||||
setTimeout(() => imageFinishedTransition(image), 5);
|
||||
} else {
|
||||
// TODO: also handle error event
|
||||
image.addEventListener("load", () => {
|
||||
image.classList.add("loaded");
|
||||
setTimeout(() => imageFinishedTransition(image), 500);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function setupPage() {
|
||||
const pageElement = document.getElementById("page");
|
||||
const pageContents = await fetchPageContents(pageData.slug);
|
||||
@ -152,6 +179,7 @@ async function setupPage() {
|
||||
document.body.classList.add("animate-element-transition");
|
||||
}, 150);
|
||||
|
||||
setTimeout(setupLazyImages, 5);
|
||||
setupCarousels();
|
||||
setupDynamicRelativeTime();
|
||||
}
|
||||
|
@ -22,13 +22,16 @@ var (
|
||||
RedditCardsHorizontalTemplate = compileTemplate("reddit-horizontal-cards.html", "widget-base.html")
|
||||
RedditCardsVerticalTemplate = compileTemplate("reddit-vertical-cards.html", "widget-base.html")
|
||||
ReleasesTemplate = compileTemplate("releases.html", "widget-base.html")
|
||||
VideosTemplate = compileTemplate("videos.html", "widget-base.html")
|
||||
VideosTemplate = compileTemplate("videos.html", "widget-base.html", "video-card-contents.html")
|
||||
VideosGridTemplate = compileTemplate("videos-grid.html", "widget-base.html", "video-card-contents.html")
|
||||
StocksTemplate = compileTemplate("stocks.html", "widget-base.html")
|
||||
RSSListTemplate = compileTemplate("rss-list.html", "widget-base.html")
|
||||
RSSCardsTemplate = compileTemplate("rss-cards.html", "widget-base.html")
|
||||
RSSHorizontalCardsTemplate = compileTemplate("rss-horizontal-cards.html", "widget-base.html")
|
||||
RSSHorizontalCards2Template = compileTemplate("rss-horizontal-cards-2.html", "widget-base.html")
|
||||
MonitorTemplate = compileTemplate("monitor.html", "widget-base.html")
|
||||
TwitchGamesListTemplate = compileTemplate("twitch-games-list.html", "widget-base.html")
|
||||
TwitchChannelsTemplate = compileTemplate("twitch-channels.html", "widget-base.html")
|
||||
RepositoryTemplate = compileTemplate("repository.html", "widget-base.html")
|
||||
)
|
||||
|
||||
var globalTemplateFunctions = template.FuncMap{
|
||||
|
@ -1,23 +1,37 @@
|
||||
{{ template "widget-base.html" . }}
|
||||
|
||||
{{ define "widget-content" }}
|
||||
{{ if ne .Style "dynamic-columns-experimental" }}
|
||||
<ul class="list list-gap-24 list-with-separator">
|
||||
{{ range .Groups }}
|
||||
<li class="bookmarks-group"{{ if .Color }} style="--bookmarks-group-color: {{ .Color.AsCSSValue }}"{{ end }}>
|
||||
{{ if ne .Title "" }}<div class="bookmarks-group-title size-h3 margin-bottom-3">{{ .Title }}</div>{{ end }}
|
||||
<ul class="list list-gap-2">
|
||||
{{ range .Links }}
|
||||
<li class="flex items-center gap-10">
|
||||
{{ if ne "" .Icon }}
|
||||
<div class="bookmarks-icon-container">
|
||||
<img class="bookmarks-icon{{ if .IsSimpleIcon }} simple-icon{{ end }}" src="{{ .Icon }}" alt="" loading="lazy">
|
||||
</div>
|
||||
{{ end }}
|
||||
<a href="{{ .URL }}" class="bookmarks-link {{ if .HideArrow }}bookmarks-link-no-arrow {{ end }}color-highlight size-h4" {{ if not .SameTab }}target="_blank"{{ end }} rel="noreferrer">{{ .Title }}</a>
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
{{ template "group" . }}
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
{{ else }}
|
||||
<div class="dynamic-columns">
|
||||
{{ range .Groups }}
|
||||
<div class="bookmarks-group"{{ if .Color }} style="--bookmarks-group-color: {{ .Color.AsCSSValue }}"{{ end }}>
|
||||
{{ template "group" . }}
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ define "group" }}
|
||||
{{ if ne .Title "" }}<div class="bookmarks-group-title size-h3 margin-bottom-3">{{ .Title }}</div>{{ end }}
|
||||
<ul class="list list-gap-2">
|
||||
{{ range .Links }}
|
||||
<li class="flex items-center gap-10">
|
||||
{{ if ne "" .Icon }}
|
||||
<div class="bookmarks-icon-container">
|
||||
<img class="bookmarks-icon{{ if .IsSimpleIcon }} simple-icon{{ end }}" src="{{ .Icon }}" alt="" loading="lazy">
|
||||
</div>
|
||||
{{ end }}
|
||||
<a href="{{ .URL }}" class="bookmarks-link {{ if .HideArrow }}bookmarks-link-no-arrow {{ end }}color-highlight size-h4" {{ if not .SameTab }}target="_blank"{{ end }} rel="noreferrer">{{ .Title }}</a>
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
{{ end }}
|
||||
|
@ -1,3 +1,7 @@
|
||||
{{ if .Page.ShowMobileHeader }}
|
||||
<div class="mobile-reachability-header">{{ .Page.Title }}</div>
|
||||
{{ end }}
|
||||
|
||||
<div class="page-columns">
|
||||
{{ range .Page.Columns }}
|
||||
<div class="page-column page-column-{{ .Size }}">
|
||||
|
@ -1,39 +1,53 @@
|
||||
{{ template "widget-base.html" . }}
|
||||
|
||||
{{ define "widget-content" }}
|
||||
{{ if ne .Style "dynamic-columns-experimental" }}
|
||||
<ul class="list list-gap-20 list-with-separator">
|
||||
{{ range .Sites }}
|
||||
<li class="monitor-site flex items-center gap-15">
|
||||
{{ if .IconUrl }}
|
||||
<img class="monitor-site-icon" src="{{ .IconUrl }}" alt="" loading="lazy">
|
||||
{{ end }}
|
||||
<div>
|
||||
<a class="size-h3 color-highlight" href="{{ .Url }}" {{ if not .SameTab }}target="_blank"{{ end }} rel="noreferrer">{{ .Title }}</a>
|
||||
<ul class="list-horizontal-text">
|
||||
{{ if not .Status.Error }}
|
||||
<li>{{ .StatusText }}</li>
|
||||
<li>{{ .Status.ResponseTime.Milliseconds | formatNumber }}ms</li>
|
||||
{{ else if .Status.TimedOut }}
|
||||
<li class="color-negative">Timed Out</li>
|
||||
{{ else }}
|
||||
<li class="color-negative" title="{{ .Status.Error }}">ERROR</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
</div>
|
||||
{{ if eq .StatusStyle "good" }}
|
||||
<div class="monitor-site-status-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="var(--color-positive)">
|
||||
<path fill-rule="evenodd" d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm13.36-1.814a.75.75 0 1 0-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 0 0-1.06 1.06l2.25 2.25a.75.75 0 0 0 1.14-.094l3.75-5.25Z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
{{ else }}
|
||||
<div class="monitor-site-status-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="var(--color-negative)">
|
||||
<path fill-rule="evenodd" d="M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003ZM12 8.25a.75.75 0 0 1 .75.75v3.75a.75.75 0 0 1-1.5 0V9a.75.75 0 0 1 .75-.75Zm0 8.25a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ template "site" . }}
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
{{ else }}
|
||||
<ul class="dynamic-columns">
|
||||
{{ range .Sites }}
|
||||
<div class="flex items-center gap-15">
|
||||
{{ template "site" . }}
|
||||
</div>
|
||||
{{ end }}
|
||||
</ul>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ define "site" }}
|
||||
{{ if .IconUrl }}
|
||||
<img class="monitor-site-icon" src="{{ .IconUrl }}" alt="" loading="lazy">
|
||||
{{ end }}
|
||||
<div>
|
||||
<a class="size-h3 color-highlight" href="{{ .Url }}" {{ if not .SameTab }}target="_blank"{{ end }} rel="noreferrer">{{ .Title }}</a>
|
||||
<ul class="list-horizontal-text">
|
||||
{{ if not .Status.Error }}
|
||||
<li>{{ .StatusText }}</li>
|
||||
<li>{{ .Status.ResponseTime.Milliseconds | formatNumber }}ms</li>
|
||||
{{ else if .Status.TimedOut }}
|
||||
<li class="color-negative">Timed Out</li>
|
||||
{{ else }}
|
||||
<li class="color-negative" title="{{ .Status.Error }}">ERROR</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
</div>
|
||||
{{ if eq .StatusStyle "good" }}
|
||||
<div class="monitor-site-status-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="var(--color-positive)">
|
||||
<path fill-rule="evenodd" d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm13.36-1.814a.75.75 0 1 0-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 0 0-1.06 1.06l2.25 2.25a.75.75 0 0 0 1.14-.094l3.75-5.25Z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
{{ else }}
|
||||
<div class="monitor-site-status-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="var(--color-negative)">
|
||||
<path fill-rule="evenodd" d="M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003ZM12 8.25a.75.75 0 0 1 .75.75v3.75a.75.75 0 0 1-1.5 0V9a.75.75 0 0 1 .75-.75Zm0 8.25a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
@ -1,7 +1,7 @@
|
||||
{{ template "widget-base.html" . }}
|
||||
|
||||
{{ define "widget-content" }}
|
||||
<ul class="list list-gap-14 list-collapsible">
|
||||
<ul class="list list-gap-10 list-collapsible">
|
||||
{{ range $i, $release := .Releases }}
|
||||
<li {{ if shouldCollapse $i $.CollapseAfter }}class="list-collapsible-item" style="--animation-delay: {{ itemAnimationDelay $i $.CollapseAfter }};"{{ end }}>
|
||||
<a class="size-h4 block text-truncate color-primary-if-not-visited" href="{{ $release.NotesUrl }}" target="_blank" rel="noreferrer">{{ .Name }}</a>
|
||||
|
44
internal/assets/templates/repository.html
Normal file
44
internal/assets/templates/repository.html
Normal file
@ -0,0 +1,44 @@
|
||||
{{ template "widget-base.html" . }}
|
||||
|
||||
{{ define "widget-content" }}
|
||||
<a class="size-h4 color-highlight" href="https://github.com/{{ $.RepositoryDetails.Name }}" target="_blank" rel="noreferrer">{{ .RepositoryDetails.Name }}</a>
|
||||
<ul class="list-horizontal-text">
|
||||
<li>{{ .RepositoryDetails.Stars | formatNumber }} stars</li>
|
||||
<li>{{ .RepositoryDetails.Forks | formatNumber }} forks</li>
|
||||
</ul>
|
||||
|
||||
{{ if gt (len .RepositoryDetails.PullRequests) 0 }}
|
||||
<hr class="margin-block-10">
|
||||
<a class="text-compact" href="https://github.com/{{ $.RepositoryDetails.Name }}/pulls" target="_blank" rel="noreferrer">Open pull requests ({{ .RepositoryDetails.OpenPullRequests | formatNumber }} total)</a>
|
||||
<div class="flex gap-7 size-h5 margin-top-3">
|
||||
<ul class="list list-gap-2">
|
||||
{{ range .RepositoryDetails.PullRequests }}
|
||||
<li title="{{ .CreatedAt | formatTime }}" {{ dynamicRelativeTimeAttrs .CreatedAt }}>{{ .CreatedAt | relativeTime }}</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
<ul class="list list-gap-2 min-width-0">
|
||||
{{ range .RepositoryDetails.PullRequests }}
|
||||
<li><a class="color-primary-if-not-visited text-truncate block" title="{{ .Title }}" target="_blank" rel="noreferrer" href="https://github.com/{{ $.RepositoryDetails.Name }}/pull/{{ .Number }}">{{ .Title }}</a></li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{ if gt (len .RepositoryDetails.Issues) 0 }}
|
||||
<hr class="margin-block-10">
|
||||
<a class="text-compact" href="https://github.com/{{ $.RepositoryDetails.Name }}/issues" target="_blank" rel="noreferrer">Open issues ({{ .RepositoryDetails.OpenIssues | formatNumber }} total)</a>
|
||||
<div class="flex gap-7 size-h5 margin-top-3">
|
||||
<ul class="list list-gap-2">
|
||||
{{ range .RepositoryDetails.Issues }}
|
||||
<li title="{{ .CreatedAt | formatTime }}" {{ dynamicRelativeTimeAttrs .CreatedAt }}>{{ .CreatedAt | relativeTime }}</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
<ul class="list list-gap-2 min-width-0">
|
||||
{{ range .RepositoryDetails.Issues }}
|
||||
<li><a class="color-primary-if-not-visited text-truncate block" title="{{ .Title }}" target="_blank" rel="noreferrer" href="https://github.com/{{ $.RepositoryDetails.Name }}/issues/{{ .Number }}">{{ .Title }}</a></li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{ end }}
|
28
internal/assets/templates/rss-horizontal-cards-2.html
Normal file
28
internal/assets/templates/rss-horizontal-cards-2.html
Normal file
@ -0,0 +1,28 @@
|
||||
{{ template "widget-base.html" . }}
|
||||
|
||||
{{ define "widget-content-classes" }}widget-content-frameless{{ end }}
|
||||
|
||||
{{ define "widget-content" }}
|
||||
<div class="carousel-container">
|
||||
<div class="cards-horizontal carousel-items-container"{{ if ne 0.0 .CardHeight }} style="--rss-card-height: {{ .CardHeight }}rem;"{{ end }}>
|
||||
{{ range .Items }}
|
||||
<div class="card rss-card-2 widget-content-frame thumbnail-container">
|
||||
{{ if ne "" .ImageURL }}
|
||||
<img class="rss-card-2-image thumbnail" loading="lazy" src="{{ .ImageURL }}" alt="">
|
||||
{{ else }}
|
||||
<svg class="rss-card-2-image" style="transform: scale(0.35) translateY(-25%)" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="var(--color-text-subdue)">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" />
|
||||
</svg>
|
||||
{{ end }}
|
||||
<div class="rss-card-2-content padding-inline-widget">
|
||||
<a href="{{ .Link }}" title="{{ .Title }}" class="block text-truncate color-primary-if-not-visited" target="_blank" rel="noreferrer">{{ .Title }}</a>
|
||||
<ul class="list-horizontal-text flex-nowrap margin-top-5">
|
||||
<li class="shrink-0" title="{{ .PublishedAt | formatTime }}" {{ dynamicRelativeTimeAttrs .PublishedAt }}>{{ .PublishedAt | relativeTime }}</li>
|
||||
<li class="shrink min-width-0 text-truncate">{{ .ChannelName }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
@ -4,7 +4,7 @@
|
||||
|
||||
{{ define "widget-content" }}
|
||||
<div class="carousel-container">
|
||||
<div class="cards-horizontal carousel-items-container">
|
||||
<div class="cards-horizontal carousel-items-container"{{ if ne 0.0 .ThumbnailHeight }} style="--rss-thumbnail-height: {{ .ThumbnailHeight }}rem;"{{ end }}>
|
||||
{{ range .Items }}
|
||||
<div class="card widget-content-frame thumbnail-container">
|
||||
{{ if ne "" .ImageURL }}
|
@ -1,23 +1,39 @@
|
||||
{{ template "widget-base.html" . }}
|
||||
|
||||
{{ define "widget-content" }}
|
||||
{{ if ne .Style "dynamic-columns-experimental" }}
|
||||
<ul class="list list-gap-20 list-with-separator">
|
||||
{{ range .Stocks }}
|
||||
<li class="flex items-center gap-15">
|
||||
<div class="shrink min-width-0">
|
||||
<div class="color-highlight size-h3 text-truncate">{{ .Symbol }}</div>
|
||||
<div class="text-truncate">{{ .Name }}</div>
|
||||
</div>
|
||||
|
||||
<svg class="stock-chart shrink-0" viewBox="0 0 100 50">
|
||||
<polyline fill="none" stroke="var(--color-text-subdue)" stroke-width="1.5px" points="{{ .SvgChartPoints }}" vector-effect="non-scaling-stroke"></polyline>
|
||||
</svg>
|
||||
|
||||
<div class="stock-values shrink-0">
|
||||
<div class="size-h3 text-right {{ if eq .PercentChange 0.0 }}{{ else if gt .PercentChange 0.0 }}color-positive{{ else }}color-negative{{ end }}">{{ printf "%+.2f" .PercentChange }}%</div>
|
||||
<div class="text-right">{{ .Currency }}{{ .Price | formatPrice }}</div>
|
||||
</div>
|
||||
{{ template "stock" . }}
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
{{ else }}
|
||||
<div class="dynamic-columns">
|
||||
{{ range .Stocks }}
|
||||
<div class="flex items-center gap-15">
|
||||
{{ template "stock" . }}
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ define "stock" }}
|
||||
<div class="shrink min-width-0">
|
||||
<a{{ if ne "" .SymbolLink }} href="{{ .SymbolLink }}" target="_blank" rel="noreferrer"{{ end }} class="color-highlight size-h3 block text-truncate">{{ .Symbol }}</a>
|
||||
<div class="text-truncate">{{ .Name }}</div>
|
||||
</div>
|
||||
|
||||
<a class="stock-chart" {{ if ne "" .ChartLink }} href="{{ .ChartLink }}" target="_blank" rel="noreferrer"{{ end }}>
|
||||
<svg class="stock-chart shrink-0" viewBox="0 0 100 50">
|
||||
<polyline fill="none" stroke="var(--color-text-subdue)" stroke-width="1.5px" points="{{ .SvgChartPoints }}" vector-effect="non-scaling-stroke"></polyline>
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
<div class="stock-values shrink-0">
|
||||
<div class="size-h3 text-right {{ if eq .PercentChange 0.0 }}{{ else if gt .PercentChange 0.0 }}color-positive{{ else }}color-negative{{ end }}">{{ printf "%+.2f" .PercentChange }}%</div>
|
||||
<div class="text-right">{{ .Currency }}{{ .Price | formatPrice }}</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
12
internal/assets/templates/video-card-contents.html
Normal file
12
internal/assets/templates/video-card-contents.html
Normal file
@ -0,0 +1,12 @@
|
||||
{{ define "video-card-contents" }}
|
||||
<img class="video-thumbnail thumbnail" loading="lazy" src="{{ .ThumbnailUrl }}" alt="">
|
||||
<div class="margin-top-10 margin-bottom-widget flex flex-column grow padding-inline-widget">
|
||||
<a class="video-title color-primary-if-not-visited" href="{{ .Url }}" target="_blank" rel="noreferrer" title="{{ .Title }}">{{ .Title }}</a>
|
||||
<ul class="list-horizontal-text flex-nowrap margin-top-7">
|
||||
<li class="shrink-0" title="{{ .TimePosted | formatTime }}" {{ dynamicRelativeTimeAttrs .TimePosted }}>{{ .TimePosted | relativeTime }}</li>
|
||||
<li class="shrink min-width-0">
|
||||
<a class="block text-truncate" href="{{ .AuthorUrl }}" target="_blank" rel="noreferrer">{{ .Author }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{{ end }}
|
13
internal/assets/templates/videos-grid.html
Normal file
13
internal/assets/templates/videos-grid.html
Normal file
@ -0,0 +1,13 @@
|
||||
{{ template "widget-base.html" . }}
|
||||
|
||||
{{ define "widget-content-classes" }}widget-content-frameless{{ end }}
|
||||
|
||||
{{ define "widget-content" }}
|
||||
<div class="cards-grid">
|
||||
{{ range .Videos }}
|
||||
<div class="card widget-content-frame thumbnail-container">
|
||||
{{ template "video-card-contents" . }}
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
@ -4,19 +4,10 @@
|
||||
|
||||
{{ define "widget-content" }}
|
||||
<div class="carousel-container">
|
||||
<div class="videos cards-horizontal carousel-items-container">
|
||||
<div class="cards-horizontal carousel-items-container">
|
||||
{{ range .Videos }}
|
||||
<div class="card widget-content-frame thumbnail-container">
|
||||
<img class="video-thumbnail thumbnail" loading="lazy" src="{{ .ThumbnailUrl }}" alt="">
|
||||
<div class="margin-top-10 margin-bottom-widget flex flex-column grow padding-inline-widget">
|
||||
<a class="video-title color-primary-if-not-visited" href="{{ .Url }}" target="_blank" rel="noreferrer" title="{{ .Title }}">{{ .Title }}</a>
|
||||
<ul class="list-horizontal-text flex-nowrap margin-top-7">
|
||||
<li class="shrink-0" title="{{ .TimePosted | formatTime }}" {{ dynamicRelativeTimeAttrs .TimePosted }}>{{ .TimePosted | relativeTime }}</li>
|
||||
<li class="shrink min-width-0">
|
||||
<a class="block text-truncate" href="{{ .AuthorUrl }}" target="_blank" rel="noreferrer">{{ .Author }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{{ template "video-card-contents" . }}
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
@ -115,3 +116,133 @@ func FetchLatestReleasesFromGithub(repositories []string, token string) (AppRele
|
||||
|
||||
return appReleases, nil
|
||||
}
|
||||
|
||||
type GithubTicket struct {
|
||||
Number int
|
||||
CreatedAt time.Time
|
||||
Title string
|
||||
}
|
||||
|
||||
type RepositoryDetails struct {
|
||||
Name string
|
||||
Stars int
|
||||
Forks int
|
||||
OpenPullRequests int
|
||||
PullRequests []GithubTicket
|
||||
OpenIssues int
|
||||
Issues []GithubTicket
|
||||
}
|
||||
|
||||
type githubRepositoryDetailsResponseJson struct {
|
||||
Name string `json:"full_name"`
|
||||
Stars int `json:"stargazers_count"`
|
||||
Forks int `json:"forks_count"`
|
||||
}
|
||||
|
||||
type githubTicketResponseJson struct {
|
||||
Count int `json:"total_count"`
|
||||
Tickets []struct {
|
||||
Number int `json:"number"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
Title string `json:"title"`
|
||||
} `json:"items"`
|
||||
}
|
||||
|
||||
func FetchRepositoryDetailsFromGithub(repository string, token string, maxPRs int, maxIssues int) (RepositoryDetails, error) {
|
||||
repositoryRequest, err := http.NewRequest("GET", fmt.Sprintf("https://api.github.com/repos/%s", repository), nil)
|
||||
|
||||
if err != nil {
|
||||
return RepositoryDetails{}, fmt.Errorf("%w: could not create request with repository: %v", ErrNoContent, err)
|
||||
}
|
||||
|
||||
PRsRequest, _ := http.NewRequest("GET", fmt.Sprintf("https://api.github.com/search/issues?q=is:pr+is:open+repo:%s&per_page=%d", repository, maxPRs), nil)
|
||||
issuesRequest, _ := http.NewRequest("GET", fmt.Sprintf("https://api.github.com/search/issues?q=is:issue+is:open+repo:%s&per_page=%d", repository, maxIssues), nil)
|
||||
|
||||
if token != "" {
|
||||
token = fmt.Sprintf("Bearer %s", token)
|
||||
repositoryRequest.Header.Add("Authorization", token)
|
||||
PRsRequest.Header.Add("Authorization", token)
|
||||
issuesRequest.Header.Add("Authorization", token)
|
||||
}
|
||||
|
||||
var detailsResponse githubRepositoryDetailsResponseJson
|
||||
var detailsErr error
|
||||
var PRsResponse githubTicketResponseJson
|
||||
var PRsErr error
|
||||
var issuesResponse githubTicketResponseJson
|
||||
var issuesErr error
|
||||
var wg sync.WaitGroup
|
||||
|
||||
wg.Add(1)
|
||||
go (func() {
|
||||
defer wg.Done()
|
||||
detailsResponse, detailsErr = decodeJsonFromRequest[githubRepositoryDetailsResponseJson](defaultClient, repositoryRequest)
|
||||
})()
|
||||
|
||||
if maxPRs > 0 {
|
||||
wg.Add(1)
|
||||
go (func() {
|
||||
defer wg.Done()
|
||||
PRsResponse, PRsErr = decodeJsonFromRequest[githubTicketResponseJson](defaultClient, PRsRequest)
|
||||
})()
|
||||
}
|
||||
|
||||
if maxIssues > 0 {
|
||||
wg.Add(1)
|
||||
go (func() {
|
||||
defer wg.Done()
|
||||
issuesResponse, issuesErr = decodeJsonFromRequest[githubTicketResponseJson](defaultClient, issuesRequest)
|
||||
})()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
if detailsErr != nil {
|
||||
return RepositoryDetails{}, fmt.Errorf("%w: could not get repository details: %s", ErrNoContent, detailsErr)
|
||||
}
|
||||
|
||||
details := RepositoryDetails{
|
||||
Name: detailsResponse.Name,
|
||||
Stars: detailsResponse.Stars,
|
||||
Forks: detailsResponse.Forks,
|
||||
PullRequests: make([]GithubTicket, 0, len(PRsResponse.Tickets)),
|
||||
Issues: make([]GithubTicket, 0, len(issuesResponse.Tickets)),
|
||||
}
|
||||
|
||||
err = nil
|
||||
|
||||
if maxPRs > 0 {
|
||||
if PRsErr != nil {
|
||||
err = fmt.Errorf("%w: could not get PRs: %s", ErrPartialContent, PRsErr)
|
||||
} else {
|
||||
details.OpenPullRequests = PRsResponse.Count
|
||||
|
||||
for i := range PRsResponse.Tickets {
|
||||
details.PullRequests = append(details.PullRequests, GithubTicket{
|
||||
Number: PRsResponse.Tickets[i].Number,
|
||||
CreatedAt: parseGithubTime(PRsResponse.Tickets[i].CreatedAt),
|
||||
Title: PRsResponse.Tickets[i].Title,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if maxIssues > 0 {
|
||||
if issuesErr != nil {
|
||||
// TODO: fix, overwriting the previous error
|
||||
err = fmt.Errorf("%w: could not get issues: %s", ErrPartialContent, issuesErr)
|
||||
} else {
|
||||
details.OpenIssues = issuesResponse.Count
|
||||
|
||||
for i := range issuesResponse.Tickets {
|
||||
details.Issues = append(details.Issues, GithubTicket{
|
||||
Number: issuesResponse.Tickets[i].Number,
|
||||
CreatedAt: parseGithubTime(issuesResponse.Tickets[i].CreatedAt),
|
||||
Title: issuesResponse.Tickets[i].Title,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return details, err
|
||||
}
|
||||
|
@ -18,8 +18,8 @@ type hackerNewsPostResponseJson struct {
|
||||
TimePosted int64 `json:"time"`
|
||||
}
|
||||
|
||||
func getHackerNewsTopPostIds() ([]int, error) {
|
||||
request, _ := http.NewRequest("GET", "https://hacker-news.firebaseio.com/v0/topstories.json", nil)
|
||||
func getHackerNewsPostIds(sort string) ([]int, error) {
|
||||
request, _ := http.NewRequest("GET", fmt.Sprintf("https://hacker-news.firebaseio.com/v0/%sstories.json", sort), nil)
|
||||
response, err := decodeJsonFromRequest[[]int](defaultClient, request)
|
||||
|
||||
if err != nil {
|
||||
@ -83,8 +83,8 @@ func getHackerNewsPostsFromIds(postIds []int, commentsUrlTemplate string) (Forum
|
||||
return posts, nil
|
||||
}
|
||||
|
||||
func FetchHackerNewsTopPosts(limit int, commentsUrlTemplate string) (ForumPosts, error) {
|
||||
postIds, err := getHackerNewsTopPostIds()
|
||||
func FetchHackerNewsPosts(sort string, limit int, commentsUrlTemplate string) (ForumPosts, error) {
|
||||
postIds, err := getHackerNewsPostIds(sort)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -85,12 +85,14 @@ var currencyToSymbol = map[string]string{
|
||||
}
|
||||
|
||||
type Stock struct {
|
||||
Name string
|
||||
Symbol string
|
||||
Currency string
|
||||
Price float64
|
||||
PercentChange float64
|
||||
SvgChartPoints string
|
||||
Name string `yaml:"name"`
|
||||
Symbol string `yaml:"symbol"`
|
||||
ChartLink string `yaml:"chart-link"`
|
||||
SymbolLink string `yaml:"symbol-link"`
|
||||
Currency string `yaml:"-"`
|
||||
Price float64 `yaml:"-"`
|
||||
PercentChange float64 `yaml:"-"`
|
||||
SvgChartPoints string `yaml:"-"`
|
||||
}
|
||||
|
||||
type Stocks []Stock
|
||||
|
@ -30,12 +30,29 @@ type subredditResponseJson struct {
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
func FetchSubredditPosts(subreddit string, commentsUrlTemplate string, requestUrlTemplate string) (ForumPosts, error) {
|
||||
subreddit = url.QueryEscape(subreddit)
|
||||
requestUrl := fmt.Sprintf("https://www.reddit.com/r/%s/hot.json", subreddit)
|
||||
func FetchSubredditPosts(subreddit, sort, topPeriod, search, commentsUrlTemplate, requestUrlTemplate string) (ForumPosts, error) {
|
||||
query := url.Values{}
|
||||
var requestUrl string
|
||||
|
||||
if search != "" {
|
||||
query.Set("q", search+" subreddit:"+subreddit)
|
||||
query.Set("sort", sort)
|
||||
}
|
||||
|
||||
if sort == "top" {
|
||||
query.Set("t", topPeriod)
|
||||
}
|
||||
|
||||
if search != "" {
|
||||
requestUrl = fmt.Sprintf("https://www.reddit.com/search.json?%s", query.Encode())
|
||||
} else {
|
||||
requestUrl = fmt.Sprintf("https://www.reddit.com/r/%s/%s.json?%s", subreddit, sort, query.Encode())
|
||||
}
|
||||
|
||||
if requestUrlTemplate != "" {
|
||||
requestUrl = strings.ReplaceAll(requestUrlTemplate, "{REQUEST-URL}", requestUrl)
|
||||
}
|
||||
|
||||
request, err := http.NewRequest("GET", requestUrl, nil)
|
||||
|
||||
if err != nil {
|
||||
@ -93,7 +110,5 @@ func FetchSubredditPosts(subreddit string, commentsUrlTemplate string, requestUr
|
||||
posts = append(posts, forumPost)
|
||||
}
|
||||
|
||||
posts.CalculateEngagement()
|
||||
|
||||
return posts, nil
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ func extractDomainFromUrl(u string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
return strings.TrimPrefix(parsed.Host, "www.")
|
||||
return strings.TrimPrefix(strings.ToLower(parsed.Host), "www.")
|
||||
}
|
||||
|
||||
func SvgPolylineCoordsFromYValues(width float64, height float64, values []float64) string {
|
||||
|
@ -24,15 +24,10 @@ type stockResponseJson struct {
|
||||
} `json:"chart"`
|
||||
}
|
||||
|
||||
type StockRequest struct {
|
||||
Symbol string
|
||||
Name string
|
||||
}
|
||||
|
||||
// TODO: allow changing chart time frame
|
||||
const stockChartDays = 21
|
||||
|
||||
func FetchStocksDataFromYahoo(stockRequests []StockRequest) (Stocks, error) {
|
||||
func FetchStocksDataFromYahoo(stockRequests Stocks) (Stocks, error) {
|
||||
requests := make([]*http.Request, 0, len(stockRequests))
|
||||
|
||||
for i := range stockRequests {
|
||||
@ -86,10 +81,12 @@ func FetchStocksDataFromYahoo(stockRequests []StockRequest) (Stocks, error) {
|
||||
}
|
||||
|
||||
stocks = append(stocks, Stock{
|
||||
Name: stockRequests[i].Name,
|
||||
Symbol: response.Chart.Result[0].Meta.Symbol,
|
||||
Price: response.Chart.Result[0].Meta.RegularMarketPrice,
|
||||
Currency: currency,
|
||||
Name: stockRequests[i].Name,
|
||||
Symbol: response.Chart.Result[0].Meta.Symbol,
|
||||
SymbolLink: stockRequests[i].SymbolLink,
|
||||
ChartLink: stockRequests[i].ChartLink,
|
||||
Price: response.Chart.Result[0].Meta.RegularMarketPrice,
|
||||
Currency: currency,
|
||||
PercentChange: percentChange(
|
||||
response.Chart.Result[0].Meta.RegularMarketPrice,
|
||||
previous,
|
||||
|
@ -55,10 +55,11 @@ type templateData struct {
|
||||
}
|
||||
|
||||
type Page struct {
|
||||
Title string `yaml:"name"`
|
||||
Slug string `yaml:"slug"`
|
||||
Columns []Column `yaml:"columns"`
|
||||
mu sync.Mutex
|
||||
Title string `yaml:"name"`
|
||||
Slug string `yaml:"slug"`
|
||||
ShowMobileHeader bool `yaml:"show-mobile-header"`
|
||||
Columns []Column `yaml:"columns"`
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (p *Page) UpdateOutdatedWidgets() {
|
||||
|
@ -22,6 +22,7 @@ type Bookmarks struct {
|
||||
HideArrow bool `yaml:"hide-arrow"`
|
||||
} `yaml:"links"`
|
||||
} `yaml:"groups"`
|
||||
Style string `yaml:"style"`
|
||||
}
|
||||
|
||||
func (widget *Bookmarks) Initialize() error {
|
||||
|
@ -13,6 +13,8 @@ type HackerNews struct {
|
||||
widgetBase `yaml:",inline"`
|
||||
Posts feed.ForumPosts `yaml:"-"`
|
||||
Limit int `yaml:"limit"`
|
||||
SortBy string `yaml:"sort-by"`
|
||||
ExtraSortBy string `yaml:"extra-sort-by"`
|
||||
CollapseAfter int `yaml:"collapse-after"`
|
||||
CommentsUrlTemplate string `yaml:"comments-url-template"`
|
||||
ShowThumbnails bool `yaml:"-"`
|
||||
@ -29,18 +31,24 @@ func (widget *HackerNews) Initialize() error {
|
||||
widget.CollapseAfter = 5
|
||||
}
|
||||
|
||||
if widget.SortBy != "top" && widget.SortBy != "new" && widget.SortBy != "best" {
|
||||
widget.SortBy = "top"
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (widget *HackerNews) Update(ctx context.Context) {
|
||||
posts, err := feed.FetchHackerNewsTopPosts(40, widget.CommentsUrlTemplate)
|
||||
posts, err := feed.FetchHackerNewsPosts(widget.SortBy, 40, widget.CommentsUrlTemplate)
|
||||
|
||||
if !widget.canContinueUpdateAfterHandlingErr(err) {
|
||||
return
|
||||
}
|
||||
|
||||
posts.CalculateEngagement()
|
||||
posts.SortByEngagement()
|
||||
if widget.ExtraSortBy == "engagement" {
|
||||
posts.CalculateEngagement()
|
||||
posts.SortByEngagement()
|
||||
}
|
||||
|
||||
if widget.Limit < len(posts) {
|
||||
posts = posts[:widget.Limit]
|
||||
|
@ -46,14 +46,15 @@ func statusCodeToStyle(status int) string {
|
||||
type Monitor struct {
|
||||
widgetBase `yaml:",inline"`
|
||||
Sites []struct {
|
||||
Title string `yaml:"title"`
|
||||
Url string `yaml:"url"`
|
||||
IconUrl string `yaml:"icon"`
|
||||
SameTab bool `yaml:"same-tab"`
|
||||
Status *feed.SiteStatus `yaml:"-"`
|
||||
StatusText string `yaml:"-"`
|
||||
StatusStyle string `yaml:"-"`
|
||||
Title string `yaml:"title"`
|
||||
Url OptionalEnvString `yaml:"url"`
|
||||
IconUrl string `yaml:"icon"`
|
||||
SameTab bool `yaml:"same-tab"`
|
||||
Status *feed.SiteStatus `yaml:"-"`
|
||||
StatusText string `yaml:"-"`
|
||||
StatusStyle string `yaml:"-"`
|
||||
} `yaml:"sites"`
|
||||
Style string `yaml:"style"`
|
||||
}
|
||||
|
||||
func (widget *Monitor) Initialize() error {
|
||||
@ -66,7 +67,7 @@ func (widget *Monitor) Update(ctx context.Context) {
|
||||
requests := make([]*http.Request, len(widget.Sites))
|
||||
|
||||
for i := range widget.Sites {
|
||||
request, err := http.NewRequest("GET", widget.Sites[i].Url, nil)
|
||||
request, err := http.NewRequest("GET", string(widget.Sites[i].Url), nil)
|
||||
|
||||
if err != nil {
|
||||
message := fmt.Errorf("failed to create http request for %s: %s", widget.Sites[i].Url, err)
|
||||
|
@ -17,6 +17,10 @@ type Reddit struct {
|
||||
Subreddit string `yaml:"subreddit"`
|
||||
Style string `yaml:"style"`
|
||||
ShowThumbnails bool `yaml:"show-thumbnails"`
|
||||
SortBy string `yaml:"sort-by"`
|
||||
TopPeriod string `yaml:"top-period"`
|
||||
Search string `yaml:"search"`
|
||||
ExtraSortBy string `yaml:"extra-sort-by"`
|
||||
CommentsUrlTemplate string `yaml:"comments-url-template"`
|
||||
Limit int `yaml:"limit"`
|
||||
CollapseAfter int `yaml:"collapse-after"`
|
||||
@ -36,6 +40,14 @@ func (widget *Reddit) Initialize() error {
|
||||
widget.CollapseAfter = 5
|
||||
}
|
||||
|
||||
if !isValidRedditSortType(widget.SortBy) {
|
||||
widget.SortBy = "hot"
|
||||
}
|
||||
|
||||
if !isValidRedditTopPeriod(widget.TopPeriod) {
|
||||
widget.TopPeriod = "day"
|
||||
}
|
||||
|
||||
if widget.RequestUrlTemplate != "" {
|
||||
if !strings.Contains(widget.RequestUrlTemplate, "{REQUEST-URL}") {
|
||||
return errors.New("no `{REQUEST-URL}` placeholder specified")
|
||||
@ -47,8 +59,32 @@ func (widget *Reddit) Initialize() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func isValidRedditSortType(sortBy string) bool {
|
||||
return sortBy == "hot" ||
|
||||
sortBy == "new" ||
|
||||
sortBy == "top" ||
|
||||
sortBy == "rising"
|
||||
}
|
||||
|
||||
func isValidRedditTopPeriod(period string) bool {
|
||||
return period == "hour" ||
|
||||
period == "day" ||
|
||||
period == "week" ||
|
||||
period == "month" ||
|
||||
period == "year" ||
|
||||
period == "all"
|
||||
}
|
||||
|
||||
func (widget *Reddit) Update(ctx context.Context) {
|
||||
posts, err := feed.FetchSubredditPosts(widget.Subreddit, widget.CommentsUrlTemplate, widget.RequestUrlTemplate)
|
||||
// TODO: refactor, use a struct to pass all of these
|
||||
posts, err := feed.FetchSubredditPosts(
|
||||
widget.Subreddit,
|
||||
widget.SortBy,
|
||||
widget.TopPeriod,
|
||||
widget.Search,
|
||||
widget.CommentsUrlTemplate,
|
||||
widget.RequestUrlTemplate,
|
||||
)
|
||||
|
||||
if !widget.canContinueUpdateAfterHandlingErr(err) {
|
||||
return
|
||||
@ -58,7 +94,11 @@ func (widget *Reddit) Update(ctx context.Context) {
|
||||
posts = posts[:widget.Limit]
|
||||
}
|
||||
|
||||
posts.SortByEngagement()
|
||||
if widget.ExtraSortBy == "engagement" {
|
||||
posts.CalculateEngagement()
|
||||
posts.SortByEngagement()
|
||||
}
|
||||
|
||||
widget.Posts = posts
|
||||
}
|
||||
|
||||
|
52
internal/widget/repository-overview.go
Normal file
52
internal/widget/repository-overview.go
Normal file
@ -0,0 +1,52 @@
|
||||
package widget
|
||||
|
||||
import (
|
||||
"context"
|
||||
"html/template"
|
||||
"time"
|
||||
|
||||
"github.com/glanceapp/glance/internal/assets"
|
||||
"github.com/glanceapp/glance/internal/feed"
|
||||
)
|
||||
|
||||
type Repository struct {
|
||||
widgetBase `yaml:",inline"`
|
||||
RequestedRepository string `yaml:"repository"`
|
||||
Token OptionalEnvString `yaml:"token"`
|
||||
PullRequestsLimit int `yaml:"pull-requests-limit"`
|
||||
IssuesLimit int `yaml:"issues-limit"`
|
||||
RepositoryDetails feed.RepositoryDetails
|
||||
}
|
||||
|
||||
func (widget *Repository) Initialize() error {
|
||||
widget.withTitle("Repository").withCacheDuration(1 * time.Hour)
|
||||
|
||||
if widget.PullRequestsLimit == 0 || widget.PullRequestsLimit < -1 {
|
||||
widget.PullRequestsLimit = 3
|
||||
}
|
||||
|
||||
if widget.IssuesLimit == 0 || widget.IssuesLimit < -1 {
|
||||
widget.IssuesLimit = 3
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (widget *Repository) Update(ctx context.Context) {
|
||||
details, err := feed.FetchRepositoryDetailsFromGithub(
|
||||
widget.RequestedRepository,
|
||||
string(widget.Token),
|
||||
widget.PullRequestsLimit,
|
||||
widget.IssuesLimit,
|
||||
)
|
||||
|
||||
if !widget.canContinueUpdateAfterHandlingErr(err) {
|
||||
return
|
||||
}
|
||||
|
||||
widget.RepositoryDetails = details
|
||||
}
|
||||
|
||||
func (widget *Repository) Render() template.HTML {
|
||||
return widget.render(widget, assets.RepositoryTemplate)
|
||||
}
|
@ -10,12 +10,14 @@ import (
|
||||
)
|
||||
|
||||
type RSS struct {
|
||||
widgetBase `yaml:",inline"`
|
||||
FeedRequests []feed.RSSFeedRequest `yaml:"feeds"`
|
||||
Style string `yaml:"style"`
|
||||
Items feed.RSSFeedItems `yaml:"-"`
|
||||
Limit int `yaml:"limit"`
|
||||
CollapseAfter int `yaml:"collapse-after"`
|
||||
widgetBase `yaml:",inline"`
|
||||
FeedRequests []feed.RSSFeedRequest `yaml:"feeds"`
|
||||
Style string `yaml:"style"`
|
||||
ThumbnailHeight float64 `yaml:"thumbnail-height"`
|
||||
CardHeight float64 `yaml:"card-height"`
|
||||
Items feed.RSSFeedItems `yaml:"-"`
|
||||
Limit int `yaml:"limit"`
|
||||
CollapseAfter int `yaml:"collapse-after"`
|
||||
}
|
||||
|
||||
func (widget *RSS) Initialize() error {
|
||||
@ -29,6 +31,14 @@ func (widget *RSS) Initialize() error {
|
||||
widget.CollapseAfter = 5
|
||||
}
|
||||
|
||||
if widget.ThumbnailHeight < 0 {
|
||||
widget.ThumbnailHeight = 0
|
||||
}
|
||||
|
||||
if widget.CardHeight < 0 {
|
||||
widget.CardHeight = 0
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -48,7 +58,11 @@ func (widget *RSS) Update(ctx context.Context) {
|
||||
|
||||
func (widget *RSS) Render() template.HTML {
|
||||
if widget.Style == "horizontal-cards" {
|
||||
return widget.render(widget, assets.RSSCardsTemplate)
|
||||
return widget.render(widget, assets.RSSHorizontalCardsTemplate)
|
||||
}
|
||||
|
||||
if widget.Style == "horizontal-cards-2" {
|
||||
return widget.render(widget, assets.RSSHorizontalCards2Template)
|
||||
}
|
||||
|
||||
return widget.render(widget, assets.RSSListTemplate)
|
||||
|
@ -9,11 +9,12 @@ import (
|
||||
"github.com/glanceapp/glance/internal/feed"
|
||||
)
|
||||
|
||||
// TODO: rename to Markets at some point
|
||||
type Stocks struct {
|
||||
widgetBase `yaml:",inline"`
|
||||
Stocks feed.Stocks `yaml:"-"`
|
||||
Sort string `yaml:"sort-by"`
|
||||
Tickers []feed.StockRequest `yaml:"stocks"`
|
||||
Stocks feed.Stocks `yaml:"stocks"`
|
||||
Sort string `yaml:"sort-by"`
|
||||
Style string `yaml:"style"`
|
||||
}
|
||||
|
||||
func (widget *Stocks) Initialize() error {
|
||||
@ -23,7 +24,7 @@ func (widget *Stocks) Initialize() error {
|
||||
}
|
||||
|
||||
func (widget *Stocks) Update(ctx context.Context) {
|
||||
stocks, err := feed.FetchStocksDataFromYahoo(widget.Tickers)
|
||||
stocks, err := feed.FetchStocksDataFromYahoo(widget.Stocks)
|
||||
|
||||
if !widget.canContinueUpdateAfterHandlingErr(err) {
|
||||
return
|
||||
|
@ -13,6 +13,7 @@ type Videos struct {
|
||||
widgetBase `yaml:",inline"`
|
||||
Videos feed.Videos `yaml:"-"`
|
||||
VideoUrlTemplate string `yaml:"video-url-template"`
|
||||
Style string `yaml:"style"`
|
||||
Channels []string `yaml:"channels"`
|
||||
Limit int `yaml:"limit"`
|
||||
}
|
||||
@ -42,5 +43,9 @@ func (widget *Videos) Update(ctx context.Context) {
|
||||
}
|
||||
|
||||
func (widget *Videos) Render() template.HTML {
|
||||
if widget.Style == "grid-cards" {
|
||||
return widget.render(widget, assets.VideosGridTemplate)
|
||||
}
|
||||
|
||||
return widget.render(widget, assets.VideosTemplate)
|
||||
}
|
||||
|
@ -43,6 +43,8 @@ func New(widgetType string) (Widget, error) {
|
||||
return &TwitchGames{}, nil
|
||||
case "twitch-channels":
|
||||
return &TwitchChannels{}, nil
|
||||
case "repository":
|
||||
return &Repository{}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown widget type: %s", widgetType)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user