Compare commits

...
67 Commits
Author SHA1 Message Date
Svilen Markov 21909e0bda Merge pull request #76 from fawni/feat/twitch-channels-sort
Add custom sorting to the twitch channels widget
2024-05-18 11:05:55 +01:00
Svilen Markov 5e9635a76f Change custom order to live 2024-05-18 11:01:34 +01:00
Svilen Markov 6627a635d5 Update build script 🤦‍♂️ 2024-05-18 10:44:52 +01:00
Svilen Markov 20740f33af Update build script 2024-05-18 10:37:14 +01:00
fawn 0681a04607 Add custom sorting to the twitch channels widget
this allows the channels to be sorted as defined in the config while
still keeping the live channels on the top. the wording could probably
be improved, "custom" is too broad but i'm not sure what else to call
it.
2024-05-18 03:07:34 +03:00
Svilen Markov 21d491843f Create SECURITY.md 2024-05-17 19:56:18 +01:00
Svilen Markov 566ac99a53 Move code of conduct 2024-05-17 19:55:34 +01:00
Svilen Markov fca504901f Create CODE_OF_CONDUCT.md 2024-05-17 19:31:03 +01:00
Svilen Markov e03231cbd7 Update time label 2024-05-17 19:28:15 +01:00
Svilen Markov 693051f4e9 Merge pull request #59 from tarikcoskun/main
Add 24-hour time format support for weather
2024-05-17 18:41:04 +01:00
Svilen Markov 06e891f9a9 Update doc 2024-05-17 18:37:35 +01:00
Svilen Markov a2bb1b88ea Remove unrelated units check 2024-05-17 18:37:11 +01:00
Svilen Markov 9602269f27 Merge pull request #49 from medinnna/main
Add web app support
2024-05-17 17:47:18 +01:00
Svilen Markov 27b9af6067 Don't transition all
Otherwise animates image dimensions when resizing from mobile to desktop
2024-05-17 17:45:50 +01:00
Svilen Markov d43c0749c1 Only apply box shadow on collapsible grids 2024-05-17 17:45:50 +01:00
Svilen Markov f4fca7600a Merge pull request #74 from brian-petersen/serve-error
Adds reporting serve errors
2024-05-17 17:31:10 +01:00
Brian Petersen 6134f1c812 Adds reporting serve errors 2024-05-17 10:11:00 -06:00
Svilen Markov 94cf82c3d9 Remove link to discussions 2024-05-17 12:21:44 +01:00
Svilen Markov b0018c3f06 Refactor, again
Make collapsible components reusable
Add ability to collapse video rows in videos-grid style
Lose sanity while dealing with all the intricacies
2024-05-17 12:15:44 +01:00
Svilen Markov 8977114806 Fix viewport offset after collapsing 2024-05-16 21:17:10 +01:00
Svilen Markov 36f8eac3e4 Refactor
Delay showing page content until JS has finished setting up page elements
That then allows the following:
Leave relative time to be rendered on the client
Leave collapsible lists to be rendered on the client
Which massively simplfies the backend templates which were error prone
2024-05-16 20:41:50 +01:00
Svilen Markov de965dace8 Increase delay before adding class 2024-05-14 16:38:54 +01:00
Svilen Markov f0ca2edb25 Reduce max width of weather columns 2024-05-14 16:36:04 +01:00
Svilen Markov 514d5f92d1 Add link to release notes 2024-05-14 14:33:24 +01:00
Svilen Markov 7e05f8a5eb Add carousel right cutoff by default
Prevents it from fading in when the page loads
2024-05-14 14:32:51 +01:00
Svilen Markov 48d4d4e966 Revert back to vh
dvh causes micro jumps when scrolling near the top of the page which looks glitchy
2024-05-14 06:28:42 +01:00
Tarık Coşkun 6a7b50b35f Add 24-hour time format support for weather 2024-05-13 15:08:58 +03:00
Svilen Markov 7743664527 Merge pull request #57 from glanceapp/release/v0.4.0
Release v0.4.0
2024-05-12 22:12:00 +01:00
Svilen Markov 714e1b6d29 Add dynamic columns style for bookmarks, stocks & monitor sites 2024-05-12 22:07:58 +01:00
Svilen Markov a0585e097f Update codeblocks highlighting 2024-05-12 20:45:57 +01:00
Svilen Markov 6363b76d33 Update codeblocks highlighting 2024-05-12 20:44:35 +01:00
Svilen Markov 503562f2e5 Update readme 2024-05-12 20:42:36 +01:00
Svilen Markov 01a7331354 Update readme with docker image build guide 2024-05-12 20:33:36 +01:00
Svilen Markov f9ca93e0b0 Account for safe area in label offset 2024-05-12 14:45:13 +01:00
Svilen Markov 09b7d71103 Revert to using the small icon for the favicon
Bit too much whitespace in the app icon to be able to use it as a favicon, makes the G look quite small
2024-05-12 14:27:25 +01:00
Svilen Markov bc06578e53 Change implementation for the safe area 2024-05-12 14:20:10 +01:00
Svilen Markov 6bb44bef20 Merge pull request #54 from eltociear/patch-1
docs: update configuration.md
2024-05-12 11:03:18 +01:00
Svilen Markov aef0c84286 Move engagement calculation inside widget 2024-05-12 11:00:45 +01:00
Svilen Markov 0fbc8dcb41 Allow specifying links for stock symbol & chart 2024-05-12 04:13:38 +01:00
Svilen Markov 818138340c Rename widget to just repository 2024-05-12 03:35:25 +01:00
Svilen Markov 4beab6bcec Make images fade in as they load 2024-05-12 03:18:55 +01:00
Ikko Eltociear Ashimine b9b7ae1cac docs: update configuration.md
minor fix
2024-05-12 03:09:12 +09:00
Carlos Medina 91c75518da Change mobile nav styles 2024-05-10 19:54:43 -06:00
Svilen Markov 82f51293df Add more refinement options for HN widget 2024-05-10 14:08:01 +01:00
Svilen Markov 840ba0f240 Add more refinement options for Reddit widget 2024-05-10 13:23:57 +01:00
Svilen Markov c5e3eed64b Add Repository Overview widget 2024-05-10 11:05:42 +01:00
Svilen Markov 7dd1cca65d Add horizontal-cards-2 style for RSS widget 2024-05-10 10:10:13 +01:00
Svilen Markov 57414bdb67 So. Many. Units. 2024-05-10 09:52:05 +01:00
Svilen Markov 646be63895 Fix 1px black border in video thumbnails 2024-05-10 08:26:55 +01:00
Svilen Markov 3c1fb97c12 Add grid-cards style for videos widget 2024-05-10 07:41:11 +01:00
Svilen Markov e78619f5e1 Update build to include openbsd and armv7 2024-05-09 13:00:42 +01:00
Svilen Markov 54102ae35e Make RSS thumbnail height configurable 2024-05-09 06:01:16 +01:00
Svilen Markov 7c4c1798b6 Lowercase domains 2024-05-09 04:49:42 +01:00
Svilen Markov f30006093d Tweak breakpoints of horizontal cards 2024-05-09 04:46:22 +01:00
Svilen Markov c8ef47064a Merge branch 'main' into release/v0.4.0 2024-05-09 04:22:51 +01:00
Svilen Markov 461c984317 Check if background color is nil before setting 2024-05-09 04:05:25 +01:00
Carlos Medina ecbad7ce6c Add mobile web app support 2024-05-08 16:51:04 -06:00
Svilen Markov 56cb97706b Document show-mobile-header property 2024-05-08 01:38:49 +01:00
Svilen Markov dd5bec799f Allow specifying Url from env 2024-05-08 01:30:02 +01:00
Svilen Markov 2389415071 Add mobile reachability header 2024-05-08 01:21:55 +01:00
Svilen Markov 976fb02bd3 Lower gap between releases 2024-05-06 16:53:24 +01:00
Svilen Markov 007fe18d27 Add utility classes 2024-05-06 16:53:13 +01:00
Svilen Markov 11f2999b52 Update readme 2024-05-06 16:52:56 +01:00
Svilen Markov e204029d3c Merge pull request #41 from chev2/catppuccin-themes
Add the other Catppuccin themes
2024-05-03 22:09:39 +01:00
Chev d3eaae2fb0 Update docs/themes.md with new Catppuccin themes 2024-05-03 12:52:02 -07:00
Chev 251c22a14f Rename catppuccin.png to catppuccin-mocha.png
For consistency
2024-05-03 12:50:53 -07:00
Chev 7d0a7186e8 Add new Catppuccin theme screenshots 2024-05-03 12:49:45 -07:00
61 changed files with 1500 additions and 296 deletions
+128
View File
@@ -0,0 +1,128 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
glanceapp@duck.com.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.
+9
View File
@@ -0,0 +1,9 @@
# Security Policy
## Supported Versions
Security updates will be applied to the latest as well as previous minor version release depending on severity and if applicable.
## Reporting a Vulnerability
Please report any suspected security vulnerabilities to [glanceapp@duck.com](mailto:glanceapp@duck.com) and do not disclose them publicly. You should receive a response within a few days and if confirmed the issue will be resolved as soon as possible.
+2 -1
View File
@@ -2,9 +2,10 @@ FROM alpine:3.19
ARG TARGETOS
ARG TARGETARCH
ARG TARGETVARIANT
WORKDIR /app
COPY build/glance-$TARGETOS-$TARGETARCH /app/glance
COPY build/glance-$TARGETOS-$TARGETARCH${TARGETVARIANT} /app/glance
EXPOSE 8080/tcp
ENTRYPOINT ["/app/glance"]
+7
View File
@@ -0,0 +1,7 @@
FROM alpine:3.19
WORKDIR /app
COPY build/glance /app/glance
EXPOSE 8080/tcp
ENTRYPOINT ["/app/glance"]
+29 -6
View File
@@ -16,6 +16,7 @@
* iframe
* Twitch channels & top games
* GitHub releases
* Repository overview
* Site monitor
#### Themeable
@@ -41,16 +42,16 @@ Checkout the [configuration docs](docs/configuration.md) to learn more. A [preco
#### Manual
Checkout the [releases page](https://github.com/glanceapp/glance/releases) for available binaries. You can place the binary inside `/opt/glance/` and have it start with your server via a [systemd service](https://linuxhandbook.com/create-systemd-services/). To specify a different path for the config file use the `--config` option:
```
```bash
/opt/glance/glance --config /etc/glance.yml
```
#### 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.
```console
```bash
docker run -d -p 8080:8080 \
-v ./glance.yml:/app/glance.yml \
-v /etc/timezone:/etc/timezone:ro \
@@ -79,12 +80,34 @@ Requirements: [Go](https://go.dev/dl/) >= v1.22
To build:
```
go build .
```bash
go build -o build/glance .
```
To run:
```
```bash
go run .
```
### Building Docker image
Build Glance with CGO disabled:
```bash
CGO_ENABLED=0 go build -o build/glance .
```
Build the image:
**Make sure to replace "owner" with your name or organization.**
```bash
docker build -t owner/glance:latest -f Dockerfile.single-platform .
```
Push the image to your registry:
```bash
docker push owner/glance:latest
```
+139 -20
View File
@@ -14,6 +14,7 @@
- [Weather](#weather)
- [Monitor](#monitor)
- [Releases](#releases)
- [Repository](#repository)
- [Bookmarks](#bookmarks)
- [Calendar](#calendar)
- [Stocks](#stocks)
@@ -155,7 +156,7 @@ Say you have a directory `glance-assets` with a file `gitea-icon.png` in it and
assets-path: /home/user/glance-assets
```
To be able to point to an asset from your assets path, use the the `/assets/` path like such:
To be able to point to an asset from your assets path, use the `/assets/` path like such:
```yaml
icon: /assets/gitea-icon.png
@@ -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/.
@@ -588,12 +647,13 @@ Example:
```yaml
- type: weather
units: metric
hour-format: 12h
location: London, United Kingdom
```
> [!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
@@ -612,6 +672,7 @@ Each bar represents a 2 hour interval. The yellow background represents sunrise
| ---- | ---- | -------- | ------- |
| location | string | yes | |
| units | string | no | metric |
| hour-format | string | no | 12h |
| hide-location | boolean | no | false |
| show-area-name | boolean | no | false |
@@ -621,6 +682,9 @@ The name of the city and country to fetch weather information for. Attempting to
##### `units`
Whether to show the temperature in celsius or fahrenheit, possible values are `metric` or `imperial`.
#### `hour-format`
Whether to show the hours of the day in 12-hour format or 24-hour format. Possible values are `12h` and `24h`.
##### `hide-location`
Optionally don't display the location name on the widget.
@@ -675,7 +739,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 +762,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 +831,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 +917,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 +992,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 +1010,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 +1037,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.
@@ -953,6 +1068,7 @@ Preview:
| ---- | ---- | -------- | ------- |
| channels | array | yes | |
| collapse-after | integer | no | 5 |
| sort-by | string | no | viewers |
##### `channels`
A list of channels to display.
@@ -960,6 +1076,9 @@ A list of channels to display.
##### `collapse-after`
How many channels are visible before the "SHOW MORE" button appears. Set to `-1` to never collapse.
##### `sort-by`
Can be used to specify the order in which the channels are displayed. Possible values are `viewers` and `live`.
### Twitch top games
Display a list of games with the most viewers on Twitch.
Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 531 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 365 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

Before

Width:  |  Height:  |  Size: 341 KiB

After

Width:  |  Height:  |  Size: 341 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

+36 -2
View File
@@ -11,8 +11,30 @@ theme:
contrast-multiplier: 1.1
```
### Catppuccin
![screenshot](images/themes/catppuccin.png)
### Catppuccin Frappe
![screenshot](images/themes/catppuccin-frappe.png)
```yaml
theme:
background-color: 229 19 23
contrast-multiplier: 1.2
primary-color: 222 74 74
positive-color: 96 44 68
negative-color: 359 68 71
```
### Catppuccin Macchiato
![screenshot](images/themes/catppuccin-macchiato.png)
```yaml
theme:
background-color: 232 23 18
contrast-multiplier: 1.2
primary-color: 220 83 75
positive-color: 105 48 72
negative-color: 351 74 73
```
### Catppuccin Mocha
![screenshot](images/themes/catppuccin-mocha.png)
```yaml
theme:
background-color: 240 21 15
@@ -42,6 +64,18 @@ theme:
## Light
### Catppuccin Latte
![screenshot](images/themes/catppuccin-latte.png)
```yaml
theme:
light: true
background-color: 220 23 95
contrast-multiplier: 1.0
primary-color: 220 91 54
positive-color: 109 58 40
negative-color: 347 87 44
```
### Peachy
![screenshot](images/themes/peachy.png)
```yaml
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

+232 -56
View File
@@ -57,6 +57,14 @@
font-size: var(--font-size-h4);
}
.page-content, .page.content-ready .page-loading-container {
display: none;
}
.page.content-ready > .page-content {
display: block;
}
.page-column-full .size-title-dynamic {
font-size: var(--font-size-h3);
}
@@ -102,12 +110,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) {
@@ -116,70 +125,76 @@
padding-top: var(--list-half-gap);
}
@keyframes listItemReveal {
.collapsible-container:not(.container-expanded) > .collapsible-item {
display: none;
}
.collapsible-item {
animation: collapsibleItemReveal .25s backwards;
}
@keyframes collapsibleItemReveal {
from {
opacity: 0;
transform: translateY(10px);
}
}
.list-collapsible-item {
display: none;
animation: listItemReveal 0.3s backwards;
animation-delay: var(--animation-delay);
}
.list-collapsible-label {
display: flex;
align-items: center;
gap: 1rem;
.expand-toggle-button {
font: inherit;
border: 0;
cursor: pointer;
display: block;
width: 100%;
text-align: left;
color: var(--color-text-base);
text-transform: uppercase;
font-size: var(--font-size-h4);
padding: var(--widget-content-vertical-padding) 0;
background: var(--color-widget-background);
}
.list-collapsible-label:has(.list-collapsible-input:checked) {
.expand-toggle-button.container-expanded {
position: sticky;
bottom: 0;
/* -1px to hide 1px gap on chrome */
bottom: -1px;
}
.list-collapsible:has(+ .list-collapsible-label > .list-collapsible-input:checked) .list-collapsible-item {
display: block;
.expand-toggle-button-icon {
display: inline-block;
margin-left: 1rem;
position: relative;
top: -.2rem;
}
.list-collapsible-input {
display: none;
}
.list-collapsible-label::before, .list-collapsible-label::after {
cursor: pointer;
display: block;
}
.list-collapsible-label::before {
content: 'SHOW MORE';
font-size: var(--font-size-h4);
}
.list-collapsible-label:has(.list-collapsible-input:checked)::before {
content: 'SHOW LESS';
}
.list-collapsible-label::after {
.expand-toggle-button-icon::before {
content: '';
font-size: 0.8rem;
transform: rotate(90deg);
line-height: 1;
display: inline-block;
transition: transform 0.3s;
}
.list-collapsible-label:has(.list-collapsible-input:checked)::after {
.expand-toggle-button.container-expanded .expand-toggle-button-icon::before {
transform: rotate(-90deg);
}
.widget-content:has(.list-collapsible-label:last-child) {
.widget-content:has(.expand-toggle-button:last-child) {
padding-bottom: 0;
}
.cards-grid.collapsible-container + .expand-toggle-button {
text-align: center;
margin-top: 0.5rem;
background-color: var(--color-background);
}
/* required to prevent collapsed lazy images from being loaded while the container is being setup */
.collapsible-container:not(.ready) img[loading=lazy] {
display: none;
}
::selection {
background-color: hsl(var(--bghs), calc(var(--scheme) (var(--scheme) var(--bgl) + 20%)));
color: var(--color-text-highlight);
@@ -205,11 +220,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;
@@ -249,6 +282,7 @@ body {
gap: var(--widget-gap);
margin: var(--widget-gap) 0;
animation: pageColumnsEntrance .3s cubic-bezier(0.25, 1, 0.5, 1) backwards;
animation-delay: 3ms;
}
@keyframes pageColumnsEntrance {
@@ -314,6 +348,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 +394,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 +439,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 +571,7 @@ body {
animation-delay: 150ms;
}
.mobile-navigation {
.mobile-navigation, .mobile-reachability-header {
display: none;
}
@@ -517,6 +598,10 @@ body {
width: 6.5rem;
}
.stock-chart svg {
width: 100%;
}
.stock-values {
min-width: 8rem;
}
@@ -553,7 +638,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;
}
@@ -636,7 +721,7 @@ body {
flex-direction: column;
width: calc(100% / 12);
padding-top: 3px;
max-width: 3.5rem;
max-width: 3rem;
}
.weather-column-value, .weather-columns:hover .weather-column-value {
@@ -788,6 +873,7 @@ body {
}
.monitor-site-status-icon {
flex-shrink: 0;
margin-left: auto;
width: 2rem;
height: 2rem;
@@ -795,8 +881,8 @@ body {
.thumbnail {
filter: grayscale(0.2) contrast(0.9);
transition: all 0.2s;
opacity: 0.8;
transition: filter 0.2s, opacity .2s;
}
.thumbnail-container:hover .thumbnail {
@@ -805,11 +891,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);
@@ -999,9 +1122,44 @@ body {
box-shadow: 0 calc(var(--spacing) * -1) 0 0 currentColor, 0 var(--spacing) 0 0 currentColor;
}
.list-collapsible-label:has(.list-collapsible-input:checked) {
.expand-toggle-button.container-expanded {
bottom: var(--mobile-navigation-height);
}
.cards-grid + .expand-toggle-button.container-expanded {
/* hides content that peeks through the rounded borders of the mobile navigation */
box-shadow: 0 var(--border-radius) 0 0 var(--color-background);
}
}
@media (max-width: 1190px) and (display-mode: standalone) {
:root {
--safe-area-inset-bottom: env(safe-area-inset-bottom, 0);
}
.list-collapsible-label:has(.list-collapsible-input:checked) {
bottom: calc(var(--mobile-navigation-height) + var(--safe-area-inset-bottom));
}
.mobile-navigation {
transform: translateY(calc(100% - var(--mobile-navigation-height) - var(--safe-area-inset-bottom)));
padding-bottom: var(--safe-area-inset-bottom);
}
.mobile-navigation-icons {
padding-bottom: var(--safe-area-inset-bottom);
transition: padding-bottom .3s;
}
.mobile-navigation-icons:has(.mobile-navigation-page-links-input:checked) {
padding-bottom: 0;
}
}
@media (display-mode: standalone) {
body {
padding-top: env(safe-area-inset-top, 0);
}
}
@media (max-width: 550px) {
@@ -1013,6 +1171,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 +1180,15 @@ body {
.hide-on-mobile {
display: none
}
.mobile-reachability-header {
display: block;
font-size: 3rem;
padding: 10vh 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 +1210,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 +1240,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; }
+204 -5
View File
@@ -21,7 +21,7 @@ function throttledDebounce(callback, maxDebounceTimes, debounceDelay) {
};
async function fetchPageContents (pageSlug) {
async function fetchPageContent(pageSlug) {
// TODO: handle non 200 status codes/time outs
// TODO: add retries
const response = await fetch(`/api/pages/${pageSlug}/content/`);
@@ -33,8 +33,13 @@ async function fetchPageContents (pageSlug) {
function setupCarousels() {
const carouselElements = document.getElementsByClassName("carousel-container");
if (carouselElements.length == 0) {
return;
}
for (let i = 0; i < carouselElements.length; i++) {
const carousel = carouselElements[i];
carousel.classList.add("show-right-cutoff");
const itemsContainer = carousel.getElementsByClassName("carousel-items-container")[0];
const determineSideCutoffs = () => {
@@ -56,7 +61,7 @@ function setupCarousels() {
itemsContainer.addEventListener("scroll", determineSideCutoffsRateLimited);
document.addEventListener("resize", determineSideCutoffsRateLimited);
determineSideCutoffs();
setTimeout(determineSideCutoffs, 1);
}
}
@@ -107,6 +112,8 @@ function setupDynamicRelativeTime() {
const updateInterval = 60 * 1000;
let lastUpdateTime = Date.now();
updateRelativeTimeForElements(elements);
const updateElementsAndTimestamp = () => {
updateRelativeTimeForElements(elements);
lastUpdateTime = Date.now();
@@ -142,18 +149,210 @@ function setupDynamicRelativeTime() {
});
}
function setupLazyImages() {
const images = document.querySelectorAll("img[loading=lazy]");
if (images.length == 0) {
return;
}
function imageFinishedTransition(image) {
image.classList.add("finished-transition");
}
setTimeout(() => {
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);
});
}
}
}, 5);
}
function attachExpandToggleButton(collapsibleContainer) {
const showMoreText = "Show more";
const showLessText = "Show less";
let expanded = false;
const button = document.createElement("button");
const icon = document.createElement("span");
icon.classList.add("expand-toggle-button-icon");
const textNode = document.createTextNode(showMoreText);
button.classList.add("expand-toggle-button");
button.append(textNode, icon);
button.addEventListener("click", () => {
expanded = !expanded;
if (expanded) {
collapsibleContainer.classList.add("container-expanded");
button.classList.add("container-expanded");
textNode.nodeValue = showLessText;
return;
}
const topBefore = button.getClientRects()[0].top;
collapsibleContainer.classList.remove("container-expanded");
button.classList.remove("container-expanded");
textNode.nodeValue = showMoreText;
const topAfter = button.getClientRects()[0].top;
if (topAfter > 0)
return;
window.scrollBy({
top: topAfter - topBefore,
behavior: "instant"
});
});
collapsibleContainer.after(button);
return button;
};
function setupCollapsibleLists() {
const collapsibleLists = document.querySelectorAll(".list.collapsible-container");
if (collapsibleLists.length == 0) {
return;
}
for (let i = 0; i < collapsibleLists.length; i++) {
const list = collapsibleLists[i];
if (list.dataset.collapseAfter === undefined) {
continue;
}
const collapseAfter = parseInt(list.dataset.collapseAfter);
if (collapseAfter == -1) {
continue;
}
if (list.children.length <= collapseAfter) {
continue;
}
attachExpandToggleButton(list);
for (let c = collapseAfter; c < list.children.length; c++) {
const child = list.children[c];
child.classList.add("collapsible-item");
child.style.animationDelay = ((c - collapseAfter) * 20).toString() + "ms";
}
list.classList.add("ready");
}
}
function setupCollapsibleGrids() {
const collapsibleGridElements = document.querySelectorAll(".cards-grid.collapsible-container");
if (collapsibleGridElements.length == 0) {
return;
}
for (let i = 0; i < collapsibleGridElements.length; i++) {
const gridElement = collapsibleGridElements[i];
if (gridElement.dataset.collapseAfterRows === undefined) {
continue;
}
const collapseAfterRows = parseInt(gridElement.dataset.collapseAfterRows);
if (collapseAfterRows == -1) {
continue;
}
const getCardsPerRow = () => {
return parseInt(getComputedStyle(gridElement).getPropertyValue('--cards-per-row'));
};
const button = attachExpandToggleButton(gridElement);
let cardsPerRow = 2;
const resolveCollapsibleItems = () => {
const hideItemsAfterIndex = cardsPerRow * collapseAfterRows;
if (hideItemsAfterIndex >= gridElement.children.length) {
button.style.display = "none";
} else {
button.style.removeProperty("display");
}
let row = 0;
for (let i = 0; i < gridElement.children.length; i++) {
const child = gridElement.children[i];
if (i >= hideItemsAfterIndex) {
child.classList.add("collapsible-item");
child.style.animationDelay = (row * 40).toString() + "ms";
if (i % cardsPerRow + 1 == cardsPerRow) {
row++;
}
} else {
child.classList.remove("collapsible-item");
child.style.removeProperty("animation-delay");
}
}
};
setTimeout(() => {
cardsPerRow = getCardsPerRow();
resolveCollapsibleItems();
gridElement.classList.add("ready");
}, 1);
window.addEventListener("resize", () => {
const newCardsPerRow = getCardsPerRow();
if (cardsPerRow == newCardsPerRow) {
return;
}
cardsPerRow = newCardsPerRow;
resolveCollapsibleItems();
});
}
}
async function setupPage() {
const pageElement = document.getElementById("page");
const pageContents = await fetchPageContents(pageData.slug);
const pageContentElement = document.getElementById("page-content");
const pageContent = await fetchPageContent(pageData.slug);
pageElement.innerHTML = pageContents;
pageContentElement.innerHTML = pageContent;
setTimeout(() => {
document.body.classList.add("animate-element-transition");
}, 150);
}, 200);
try {
setupLazyImages();
setupCarousels();
setupCollapsibleLists();
setupCollapsibleGrids();
setupDynamicRelativeTime();
} finally {
pageElement.classList.add("content-ready");
}
}
if (document.readyState === "loading") {
+13
View File
@@ -0,0 +1,13 @@
{
"name": "Glance",
"display": "standalone",
"scope": "/",
"start_url": "/",
"icons": [
{
"src": "/static/app-icon.png",
"type": "image/png",
"sizes": "512x512"
}
]
}
+5 -2
View File
@@ -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{
+23 -9
View File
@@ -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">
{{ 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>
</li>
{{ end }}
</li>
{{ end }}
</ul>
{{ end }}
+4
View File
@@ -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 }}">
+9 -1
View File
@@ -5,7 +5,15 @@
<title>{{ block "document-title" . }}{{ end }}</title>
<meta charset="UTF-8">
<meta name="color-scheme" content="dark">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="Glance">
<meta name="theme-color" content="{{ if ne nil .App.Config.Theme.BackgroundColor }}{{ .App.Config.Theme.BackgroundColor }}{{ else }}hsl(240, 8%, 9%){{ end }}">
<link rel="apple-touch-icon" sizes="512x512" href="/static/app-icon.png">
<link rel="icon" type="image/png" sizes="50x50" href="/static/favicon.png">
<link rel="manifest" href="/static/manifest.json">
<link rel="icon" type="image/png" href="/static/favicon.png" />
<link rel="stylesheet" href="/static/main.css?v={{ .App.Config.Server.StartedAt.Unix }}">
<script async src="/static/main.js?v={{ .App.Config.Server.StartedAt.Unix }}"></script>
+12 -15
View File
@@ -1,14 +1,14 @@
{{ template "widget-base.html" . }}
{{ define "widget-content" }}
<ul class="list list-gap-14 list-collapsible">
{{ range $i, $post := .Posts }}
<li {{ if shouldCollapse $i $.CollapseAfter }}class="list-collapsible-item" style="--animation-delay: {{ itemAnimationDelay $i $.CollapseAfter }};"{{ end }}>
<ul class="list list-gap-14 collapsible-container" data-collapse-after="{{ .CollapseAfter }}">
{{ range .Posts }}
<li>
<div class="forum-post-list-item thumbnail-container">
{{ if $.ShowThumbnails }}
{{ if ne $post.ThumbnailUrl "" }}
<img class="forum-post-list-thumbnail thumbnail" src="{{ $post.ThumbnailUrl }}" alt="" loading="lazy">
{{ else if $post.HasTargetUrl }}
{{ if ne .ThumbnailUrl "" }}
<img class="forum-post-list-thumbnail thumbnail" src="{{ .ThumbnailUrl }}" alt="" loading="lazy">
{{ else if .HasTargetUrl }}
<svg class="forum-post-list-thumbnail hide-on-mobile" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="-9 -8 40 40" stroke-width="1.5" stroke="var(--color-text-subdue)">
<path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244" />
</svg>
@@ -19,13 +19,13 @@
{{ end }}
{{ end }}
<div class="grow">
<a href="{{ $post.DiscussionUrl }}" class="size-h3 color-primary-if-not-visited" target="_blank" rel="noreferrer">{{ .Title }}</a>
<a href="{{ .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>
<li {{ dynamicRelativeTimeAttrs .TimePosted }}></li>
<li>{{ .Score | formatNumber }} points</li>
<li>{{ .CommentCount | formatNumber }} comments</li>
{{ if .HasTargetUrl }}
<li class="shrink min-width-0"><a class="visited-indicator text-truncate block" href="{{ .TargetUrl }}" target="_blank" rel="noreferrer">{{ .TargetUrlDomain }}</a></li>
{{ end }}
</ul>
</div>
@@ -33,7 +33,4 @@
</li>
{{ end }}
</ul>
{{ if gt (len .Posts) $.CollapseAfter }}
<label class="list-collapsible-label"><input type="checkbox" autocomplete="off" class="list-collapsible-input"></label>
{{ end }}
{{ end }}
+28 -14
View File
@@ -1,13 +1,30 @@
{{ 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">
{{ template "site" . }}
</li>
{{ end }}
<div>
</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 }}
@@ -19,21 +36,18 @@
<li class="color-negative" title="{{ .Status.Error }}">ERROR</li>
{{ end }}
</ul>
</div>
{{ if eq .StatusStyle "good" }}
<div class="monitor-site-status-icon">
</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">
</div>
{{ else }}
<div class="monitor-site-status-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="var(--color-negative)">
<path fill-rule="evenodd" d="M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003ZM12 8.25a.75.75 0 0 1 .75.75v3.75a.75.75 0 0 1-1.5 0V9a.75.75 0 0 1 .75-.75Zm0 8.25a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Z" clip-rule="evenodd" />
</svg>
</div>
{{ end }}
</li>
{{ end }}
</ul>
</div>
{{ end }}
{{ end }}
+3 -5
View File
@@ -50,6 +50,7 @@
<div class="content-bounds">
<div class="page" id="page">
<div class="page-content" id="page-content"></div>
<div class="page-loading-container">
<!-- TODO: add a bigger/better loading indicator -->
<div class="loading-icon"></div>
@@ -59,11 +60,8 @@
<div class="footer flex items-center flex-column">
<div>
<span class="size-h3">Glance</span> ({{ .App.Version }})
<a class="size-h3" href="https://github.com/glanceapp/glance" target="_blank" rel="noreferrer">Glance</a> {{ if ne "dev" .App.Version }}<a class="visited-indicator" title="Release notes" href="https://github.com/glanceapp/glance/releases/tag/{{ .App.Version }}" target="_blank" rel="noreferrer">{{ .App.Version }}</a>{{ else }}({{ .App.Version }}){{ end }}
</div>
<ul class="list-horizontal-text margin-top-5 size-h5 color-primary">
<li><a href="https://github.com/glanceapp/glance/issues" target="_blank" rel="noreferrer">Report issue</a></li>
<li><a href="https://github.com/glanceapp/glance/discussions" target="_blank" rel="noreferrer">Submit feedback</a></li>
</ul>
<a class="color-primary block margin-top-5 size-h5" href="https://github.com/glanceapp/glance/issues" target="_blank" rel="noreferrer">Report issue</a>
</div>
{{ end }}
@@ -20,7 +20,7 @@
{{ end }}
<a href="{{ .DiscussionUrl }}" title="{{ .Title }}" class="text-truncate-3-lines color-primary-if-not-visited margin-top-7 margin-bottom-auto" target="_blank" rel="noreferrer">{{ .Title }}</a>
<ul class="list-horizontal-text margin-top-7">
<li title="{{ .TimePosted | formatTime }}" {{ dynamicRelativeTimeAttrs .TimePosted }}>{{ .TimePosted | relativeTime }}</li>
<li {{ dynamicRelativeTimeAttrs .TimePosted }}></li>
<li>{{ .Score | formatNumber }} points</li>
</ul>
</div>
@@ -19,7 +19,7 @@
{{ end }}
<a href="{{ .DiscussionUrl }}" title="{{ .Title }}" class="text-truncate-3-lines color-primary-if-not-visited margin-top-7" target="_blank" rel="noreferrer">{{ .Title }}</a>
<ul class="list-horizontal-text margin-top-7">
<li title="{{ .TimePosted | formatTime }}" {{ dynamicRelativeTimeAttrs .TimePosted }}>{{ .TimePosted | relativeTime }}</li>
<li {{ dynamicRelativeTimeAttrs .TimePosted }}></li>
<li>{{ .Score | formatNumber }} points</li>
</ul>
</div>
+3 -6
View File
@@ -1,12 +1,12 @@
{{ template "widget-base.html" . }}
{{ define "widget-content" }}
<ul class="list list-gap-14 list-collapsible">
<ul class="list list-gap-10 collapsible-container" data-collapse-after="{{ .CollapseAfter }}">
{{ range $i, $release := .Releases }}
<li {{ if shouldCollapse $i $.CollapseAfter }}class="list-collapsible-item" style="--animation-delay: {{ itemAnimationDelay $i $.CollapseAfter }};"{{ end }}>
<li>
<a class="size-h4 block text-truncate color-primary-if-not-visited" href="{{ $release.NotesUrl }}" target="_blank" rel="noreferrer">{{ .Name }}</a>
<ul class="list-horizontal-text">
<li title="{{ $release.TimeReleased | formatTime }}" {{ dynamicRelativeTimeAttrs $release.TimeReleased }}>{{ $release.TimeReleased | relativeTime }}</li>
<li {{ dynamicRelativeTimeAttrs $release.TimeReleased }}></li>
<li>{{ $release.Version }}</li>
{{ if gt $release.Downvotes 3 }}
<li>{{ $release.Downvotes | formatNumber }} ⚠</li>
@@ -15,7 +15,4 @@
</li>
{{ end }}
</ul>
{{ if gt (len .Releases) $.CollapseAfter }}
<label class="list-collapsible-label"><input type="checkbox" autocomplete="off" class="list-collapsible-input"></label>
{{ end }}
{{ end }}
+44
View File
@@ -0,0 +1,44 @@
{{ template "widget-base.html" . }}
{{ define "widget-content" }}
<a class="size-h4 color-highlight" href="https://github.com/{{ $.RepositoryDetails.Name }}" target="_blank" rel="noreferrer">{{ .RepositoryDetails.Name }}</a>
<ul class="list-horizontal-text">
<li>{{ .RepositoryDetails.Stars | formatNumber }} stars</li>
<li>{{ .RepositoryDetails.Forks | formatNumber }} forks</li>
</ul>
{{ if gt (len .RepositoryDetails.PullRequests) 0 }}
<hr class="margin-block-10">
<a class="text-compact" href="https://github.com/{{ $.RepositoryDetails.Name }}/pulls" target="_blank" rel="noreferrer">Open pull requests ({{ .RepositoryDetails.OpenPullRequests | formatNumber }} total)</a>
<div class="flex gap-7 size-h5 margin-top-3">
<ul class="list list-gap-2">
{{ range .RepositoryDetails.PullRequests }}
<li {{ dynamicRelativeTimeAttrs .CreatedAt }}></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 {{ dynamicRelativeTimeAttrs .CreatedAt }}></li>
{{ end }}
</ul>
<ul class="list list-gap-2 min-width-0">
{{ range .RepositoryDetails.Issues }}
<li><a class="color-primary-if-not-visited text-truncate block" title="{{ .Title }}" target="_blank" rel="noreferrer" href="https://github.com/{{ $.RepositoryDetails.Name }}/issues/{{ .Number }}">{{ .Title }}</a></li>
{{ end }}
</ul>
</div>
{{ end }}
{{ end }}
@@ -0,0 +1,28 @@
{{ template "widget-base.html" . }}
{{ define "widget-content-classes" }}widget-content-frameless{{ end }}
{{ define "widget-content" }}
<div class="carousel-container">
<div class="cards-horizontal carousel-items-container"{{ if ne 0.0 .CardHeight }} style="--rss-card-height: {{ .CardHeight }}rem;"{{ end }}>
{{ range .Items }}
<div class="card rss-card-2 widget-content-frame thumbnail-container">
{{ if ne "" .ImageURL }}
<img class="rss-card-2-image thumbnail" loading="lazy" src="{{ .ImageURL }}" alt="">
{{ else }}
<svg class="rss-card-2-image" style="transform: scale(0.35) translateY(-25%)" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="var(--color-text-subdue)">
<path stroke-linecap="round" stroke-linejoin="round" d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" />
</svg>
{{ end }}
<div class="rss-card-2-content padding-inline-widget">
<a href="{{ .Link }}" title="{{ .Title }}" class="block text-truncate color-primary-if-not-visited" target="_blank" rel="noreferrer">{{ .Title }}</a>
<ul class="list-horizontal-text flex-nowrap margin-top-5">
<li class="shrink-0" {{ dynamicRelativeTimeAttrs .PublishedAt }}></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 }}
@@ -17,7 +17,7 @@
<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>
<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" {{ dynamicRelativeTimeAttrs .PublishedAt }}></li>
<li class="shrink min-width-0 text-truncate">{{ .ChannelName }}</li>
</ul>
</div>
+4 -7
View File
@@ -1,12 +1,12 @@
{{ template "widget-base.html" . }}
{{ define "widget-content" }}
<ul class="list list-gap-14 list-collapsible">
{{ range $i, $item := .Items }}
<li {{ if shouldCollapse $i $.CollapseAfter }}class="list-collapsible-item" style="--animation-delay: {{ itemAnimationDelay $i $.CollapseAfter }};"{{ end }}>
<ul class="list list-gap-14 collapsible-container" data-collapse-after="{{ .CollapseAfter }}">
{{ range .Items }}
<li>
<a class="size-title-dynamic color-primary-if-not-visited" href="{{ .Link }}" target="_blank" rel="noreferrer">{{ .Title }}</a>
<ul class="list-horizontal-text">
<li title="{{ $item.PublishedAt | formatTime }}" {{ dynamicRelativeTimeAttrs $item.PublishedAt }}>{{ .PublishedAt | relativeTime }}</li>
<li {{ dynamicRelativeTimeAttrs .PublishedAt }}></li>
{{ if gt (len $.FeedRequests) 1 }}
<li><a href="{{ .ChannelURL }}" target="_blank" rel="noreferrer">{{ .ChannelName }}</a></li>
{{ end }}
@@ -14,7 +14,4 @@
</li>
{{ end }}
</ul>
{{ if gt (len .Items) $.CollapseAfter }}
<label class="list-collapsible-label"><input type="checkbox" autocomplete="off" class="list-collapsible-input"></label>
{{ end }}
{{ end }}
+29 -13
View File
@@ -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 -15
View File
@@ -1,13 +1,13 @@
{{ template "widget-base.html" . }}
{{ define "widget-content" }}
<ul class="list list-gap-14 list-collapsible">
{{ range $i, $channel := .Channels }}
<li {{ if shouldCollapse $i $.CollapseAfter }}class="list-collapsible-item" style="--animation-delay: {{ itemAnimationDelay $i $.CollapseAfter }};"{{ end }}>
<div class="{{ if $channel.IsLive }}twitch-channel-live {{ end }}flex gap-10 items-start thumbnail-container">
<ul class="list list-gap-14 collapsible-container" data-collapse-after="{{ .CollapseAfter }}">
{{ range .Channels }}
<li>
<div class="{{ if .IsLive }}twitch-channel-live {{ end }}flex gap-10 items-start thumbnail-container">
<div class="twitch-channel-avatar-container">
{{ if $channel.Exists }}
<img class="twitch-channel-avatar thumbnail" src="{{ $channel.AvatarUrl }}" alt="" loading="lazy">
{{ if .Exists }}
<img class="twitch-channel-avatar thumbnail" src="{{ .AvatarUrl }}" alt="" loading="lazy">
{{ else }}
<svg class="twitch-channel-avatar thumbnail" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" />
@@ -15,13 +15,13 @@
{{ end }}
</div>
<div class="shrink min-width-0">
<a href="https://twitch.tv/{{ $channel.Login }}" class="size-h3{{ if $channel.IsLive }} color-highlight{{ end }} block text-truncate" target="_blank" rel="noreferrer">{{ $channel.Name }}</a>
{{ if $channel.Exists }}
{{ if $channel.IsLive }}
<a class="text-truncate block" href="https://www.twitch.tv/directory/category/{{ $channel.CategorySlug }}" target="_blank" rel="noreferrer">{{ $channel.Category }}</a>
<a href="https://twitch.tv/{{ .Login }}" class="size-h3{{ if .IsLive }} color-highlight{{ end }} block text-truncate" target="_blank" rel="noreferrer">{{ .Name }}</a>
{{ if .Exists }}
{{ if .IsLive }}
<a class="text-truncate block" href="https://www.twitch.tv/directory/category/{{ .CategorySlug }}" target="_blank" rel="noreferrer">{{ .Category }}</a>
<ul class="list-horizontal-text">
<li title="{{ $channel.LiveSince | formatTime }}" {{ dynamicRelativeTimeAttrs $channel.LiveSince }}>{{ $channel.LiveSince | relativeTime }}</li>
<li>{{ $channel.ViewersCount | formatViewerCount }} viewers</li>
<li {{ dynamicRelativeTimeAttrs .LiveSince }}></li>
<li>{{ .ViewersCount | formatViewerCount }} viewers</li>
</ul>
{{ else }}
<div>Offline</div>
@@ -34,7 +34,4 @@
</li>
{{ end }}
</ul>
{{ if gt (len .Channels) $.CollapseAfter }}
<label class="list-collapsible-label"><input type="checkbox" autocomplete="off" class="list-collapsible-input"></label>
{{ end }}
{{ end }}
@@ -1,22 +1,21 @@
{{ template "widget-base.html" . }}
{{ define "widget-content" }}
<ul class="list list-gap-14 list-collapsible">
{{ range $i, $category := .Categories }}
{{ $shouldCollapseItem := shouldCollapse $i $.CollapseAfter }}
<li class="twitch-category thumbnail-container{{ if $shouldCollapseItem }} list-collapsible-item{{ end }}" {{ if $shouldCollapseItem }}style="--animation-delay: {{ itemAnimationDelay $i $.CollapseAfter }};"{{ end }}>
<ul class="list list-gap-14 collapsible-container" data-collapse-after="{{ .CollapseAfter }}">
{{ range .Categories }}
<li class="twitch-category thumbnail-container">
<div class="flex gap-10 items-center">
<img class="twitch-category-thumbnail thumbnail" loading="lazy" src="{{ $category.AvatarUrl }}" alt="">
<img class="twitch-category-thumbnail thumbnail" loading="lazy" src="{{ .AvatarUrl }}" alt="">
<div class="shrink min-width-0">
<a class="size-h3 color-highlight text-truncate block" href="https://www.twitch.tv/directory/category/{{ $category.Slug }}" target="_blank" rel="noreferrer">{{ $category.Name }}</a>
<a class="size-h3 color-highlight text-truncate block" href="https://www.twitch.tv/directory/category/{{ .Slug }}" target="_blank" rel="noreferrer">{{ .Name }}</a>
<ul class="list-horizontal-text">
<li>{{ $category.ViewersCount | formatViewerCount }} viewers</li>
{{ if $category.IsNew }}
<li>{{ .ViewersCount | formatViewerCount }} viewers</li>
{{ if .IsNew }}
<li class="color-primary">NEW</li>
{{ end }}
</ul>
<ul class="list-horizontal-text flex-nowrap">
{{ range $i, $tag := $category.Tags }}
{{ range $i, $tag := .Tags }}
{{ if eq $i 0 }}
<li class="shrink-0">{{ $tag.Name }}</li>
{{ else }}
@@ -29,7 +28,4 @@
</li>
{{ end }}
</ul>
{{ if gt (len .Categories) $.CollapseAfter }}
<label class="list-collapsible-label"><input type="checkbox" autocomplete="off" class="list-collapsible-input"></label>
{{ end }}
{{ end }}
@@ -0,0 +1,12 @@
{{ define "video-card-contents" }}
<img class="video-thumbnail thumbnail" loading="lazy" src="{{ .ThumbnailUrl }}" alt="">
<div class="margin-top-10 margin-bottom-widget flex flex-column grow padding-inline-widget">
<a class="video-title color-primary-if-not-visited" href="{{ .Url }}" target="_blank" rel="noreferrer" title="{{ .Title }}">{{ .Title }}</a>
<ul class="list-horizontal-text flex-nowrap margin-top-7">
<li class="shrink-0" {{ dynamicRelativeTimeAttrs .TimePosted }}></li>
<li class="shrink min-width-0">
<a class="block text-truncate" href="{{ .AuthorUrl }}" target="_blank" rel="noreferrer">{{ .Author }}</a>
</li>
</ul>
</div>
{{ end }}
@@ -0,0 +1,13 @@
{{ template "widget-base.html" . }}
{{ define "widget-content-classes" }}widget-content-frameless{{ end }}
{{ define "widget-content" }}
<div class="cards-grid collapsible-container" data-collapse-after-rows="{{ .CollapseAfterRows }}">
{{ range .Videos }}
<div class="card widget-content-frame thumbnail-container">
{{ template "video-card-contents" . }}
</div>
{{ end }}
</div>
{{ end }}
+2 -11
View File
@@ -4,19 +4,10 @@
{{ define "widget-content" }}
<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>
+131
View File
@@ -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
}
+4 -4
View File
@@ -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
+8 -6
View File
@@ -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
+20 -5
View File
@@ -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
}
+6
View File
@@ -44,6 +44,12 @@ func (channels TwitchChannels) SortByViewers() {
})
}
func (channels TwitchChannels) SortByLive() {
sort.SliceStable(channels, func(i, j int) bool {
return channels[i].IsLive && !channels[j].IsLive
})
}
type twitchOperationResponse struct {
Data json.RawMessage
Extensions struct {
+1 -1
View File
@@ -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 {
+3 -6
View File
@@ -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 {
@@ -88,6 +83,8 @@ func FetchStocksDataFromYahoo(stockRequests []StockRequest) (Stocks, error) {
stocks = append(stocks, Stock{
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(
+1
View File
@@ -57,6 +57,7 @@ type templateData struct {
type Page struct {
Title string `yaml:"name"`
Slug string `yaml:"slug"`
ShowMobileHeader bool `yaml:"show-mobile-header"`
Columns []Column `yaml:"columns"`
mu sync.Mutex
}
+1 -1
View File
@@ -36,7 +36,7 @@ func Main() int {
return 1
}
if app.Serve() != nil {
if err := app.Serve(); err != nil {
fmt.Printf("http server error: %v\n", err)
return 1
}
+1
View File
@@ -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 {
+9 -1
View File
@@ -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
}
if widget.ExtraSortBy == "engagement" {
posts.CalculateEngagement()
posts.SortByEngagement()
}
if widget.Limit < len(posts) {
posts = posts[:widget.Limit]
+3 -2
View File
@@ -47,13 +47,14 @@ type Monitor struct {
widgetBase `yaml:",inline"`
Sites []struct {
Title string `yaml:"title"`
Url string `yaml:"url"`
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)
+41 -1
View File
@@ -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]
}
if widget.ExtraSortBy == "engagement" {
posts.CalculateEngagement()
posts.SortByEngagement()
}
widget.Posts = posts
}
+52
View File
@@ -0,0 +1,52 @@
package widget
import (
"context"
"html/template"
"time"
"github.com/glanceapp/glance/internal/assets"
"github.com/glanceapp/glance/internal/feed"
)
type Repository struct {
widgetBase `yaml:",inline"`
RequestedRepository string `yaml:"repository"`
Token OptionalEnvString `yaml:"token"`
PullRequestsLimit int `yaml:"pull-requests-limit"`
IssuesLimit int `yaml:"issues-limit"`
RepositoryDetails feed.RepositoryDetails
}
func (widget *Repository) Initialize() error {
widget.withTitle("Repository").withCacheDuration(1 * time.Hour)
if widget.PullRequestsLimit == 0 || widget.PullRequestsLimit < -1 {
widget.PullRequestsLimit = 3
}
if widget.IssuesLimit == 0 || widget.IssuesLimit < -1 {
widget.IssuesLimit = 3
}
return nil
}
func (widget *Repository) Update(ctx context.Context) {
details, err := feed.FetchRepositoryDetailsFromGithub(
widget.RequestedRepository,
string(widget.Token),
widget.PullRequestsLimit,
widget.IssuesLimit,
)
if !widget.canContinueUpdateAfterHandlingErr(err) {
return
}
widget.RepositoryDetails = details
}
func (widget *Repository) Render() template.HTML {
return widget.render(widget, assets.RepositoryTemplate)
}
+15 -1
View File
@@ -13,6 +13,8 @@ type RSS struct {
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"`
@@ -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)
+4 -3
View File
@@ -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:"-"`
Stocks feed.Stocks `yaml:"stocks"`
Sort string `yaml:"sort-by"`
Tickers []feed.StockRequest `yaml:"stocks"`
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
+10
View File
@@ -14,6 +14,7 @@ type TwitchChannels struct {
ChannelsRequest []string `yaml:"channels"`
Channels []feed.TwitchChannel `yaml:"-"`
CollapseAfter int `yaml:"collapse-after"`
SortBy string `yaml:"sort-by"`
}
func (widget *TwitchChannels) Initialize() error {
@@ -23,6 +24,10 @@ func (widget *TwitchChannels) Initialize() error {
widget.CollapseAfter = 5
}
if widget.SortBy != "viewers" && widget.SortBy != "live" {
widget.SortBy = "viewers"
}
return nil
}
@@ -33,7 +38,12 @@ func (widget *TwitchChannels) Update(ctx context.Context) {
return
}
if widget.SortBy == "viewers" {
channels.SortByViewers()
} else if widget.SortBy == "live" {
channels.SortByLive()
}
widget.Channels = channels
}
+10
View File
@@ -13,6 +13,8 @@ type Videos struct {
widgetBase `yaml:",inline"`
Videos feed.Videos `yaml:"-"`
VideoUrlTemplate string `yaml:"video-url-template"`
Style string `yaml:"style"`
CollapseAfterRows int `yaml:"collapse-after-rows"`
Channels []string `yaml:"channels"`
Limit int `yaml:"limit"`
}
@@ -24,6 +26,10 @@ func (widget *Videos) Initialize() error {
widget.Limit = 25
}
if widget.CollapseAfterRows == 0 || widget.CollapseAfterRows < -1 {
widget.CollapseAfterRows = 4
}
return nil
}
@@ -42,5 +48,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)
}
+11 -2
View File
@@ -14,17 +14,26 @@ type Weather struct {
Location string `yaml:"location"`
ShowAreaName bool `yaml:"show-area-name"`
HideLocation bool `yaml:"hide-location"`
HourFormat string `yaml:"hour-format"`
Units string `yaml:"units"`
Place *feed.PlaceJson `yaml:"-"`
Weather *feed.Weather `yaml:"-"`
TimeLabels [12]string `yaml:"-"`
}
var timeLabels = [12]string{"2am", "4am", "6am", "8am", "10am", "12pm", "2pm", "4pm", "6pm", "8pm", "10pm", "12am"}
var timeLabels12h = [12]string{"2am", "4am", "6am", "8am", "10am", "12pm", "2pm", "4pm", "6pm", "8pm", "10pm", "12am"}
var timeLabels24h = [12]string{"02:00", "04:00", "06:00", "08:00", "10:00", "12:00", "14:00", "16:00", "18:00", "20:00", "22:00", "00:00"}
func (widget *Weather) Initialize() error {
widget.withTitle("Weather").withCacheOnTheHour()
widget.TimeLabels = timeLabels
if widget.HourFormat == "" || widget.HourFormat == "12h" {
widget.TimeLabels = timeLabels12h
} else if widget.HourFormat == "24h" {
widget.TimeLabels = timeLabels24h
} else {
return fmt.Errorf("invalid hour format '%s' for weather widget, must be either 12h or 24h", widget.HourFormat)
}
if widget.Units == "" {
widget.Units = "metric"
+2
View File
@@ -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)
}
+72 -6
View File
@@ -1,6 +1,7 @@
package main
import (
"flag"
"fmt"
"os"
"os/exec"
@@ -67,9 +68,50 @@ var buildTargets = []buildTarget{
arch: "arm",
armV: 7,
},
{
os: "openbsd",
arch: "amd64",
},
{
os: "openbsd",
arch: "386",
},
}
func hasUncommitedChanges() (bool, error) {
output, err := exec.Command("git", "status", "--porcelain").CombinedOutput()
if err != nil {
return false, err
}
return len(output) > 0, nil
}
func main() {
flags := flag.NewFlagSet("", flag.ExitOnError)
specificTag := flags.String("tag", "", "Which tagged version to build")
err := flags.Parse(os.Args[1:])
if err != nil {
fmt.Println(err)
os.Exit(1)
}
uncommitedChanges, err := hasUncommitedChanges()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
if uncommitedChanges {
fmt.Println("There are uncommited changes - commit, stash or discard them first")
os.Exit(1)
}
cwd, err := os.Getwd()
if err != nil {
@@ -87,12 +129,26 @@ func main() {
os.Mkdir(buildPath, 0755)
os.Mkdir(archivesPath, 0755)
var version string
if *specificTag == "" {
version, err := getVersionFromGit()
if err != nil {
fmt.Println(version, err)
os.Exit(1)
}
} else {
version = *specificTag
}
output, err := exec.Command("git", "checkout", "tags/"+version).CombinedOutput()
if err != nil {
fmt.Println(string(output))
fmt.Println(err)
os.Exit(1)
}
info := buildInfo{
version: version,
@@ -111,13 +167,19 @@ func main() {
fmt.Println("Building docker image")
output, err := exec.Command(
"sudo", "docker", "build",
"--platform=linux/arm64,linux/amd64",
var dockerBuildOptions = []string{
"docker", "build",
"--platform=linux/amd64,linux/arm64,linux/arm/v7",
"-t", versionTag,
"-t", latestTag,
".",
).CombinedOutput()
}
if !strings.Contains(version, "beta") {
dockerBuildOptions = append(dockerBuildOptions, "-t", latestTag)
}
dockerBuildOptions = append(dockerBuildOptions, ".")
output, err = exec.Command("sudo", dockerBuildOptions...).CombinedOutput()
if err != nil {
fmt.Println(string(output))
@@ -144,6 +206,10 @@ func main() {
os.Exit(1)
}
if strings.Contains(version, "beta") {
return
}
output, err = exec.Command(
"sudo", "docker", "push", latestTag,
).CombinedOutput()