diff --git a/.gitignore b/.gitignore
index f7e0f6c..e466992 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
/assets
/build
/playground
+/.idea
glance*.yml
diff --git a/Dockerfile b/Dockerfile
index e4019ba..b89541a 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM golang:1.22.5-alpine3.20 AS builder
+FROM golang:1.23.1-alpine3.20 AS builder
WORKDIR /app
COPY . /app
@@ -9,5 +9,8 @@ FROM alpine:3.20
WORKDIR /app
COPY --from=builder /app/glance .
+HEALTHCHECK --timeout=10s --start-period=60s --interval=60s \
+ CMD wget --spider -q http://localhost:8080/api/healthz
+
EXPOSE 8080/tcp
-ENTRYPOINT ["/app/glance"]
+ENTRYPOINT ["/app/glance", "--config", "/app/config/glance.yml"]
diff --git a/Dockerfile.goreleaser b/Dockerfile.goreleaser
index dec9ac4..eaf8336 100644
--- a/Dockerfile.goreleaser
+++ b/Dockerfile.goreleaser
@@ -3,6 +3,8 @@ FROM alpine:3.20
WORKDIR /app
COPY glance .
-EXPOSE 8080/tcp
+HEALTHCHECK --timeout=10s --start-period=60s --interval=60s \
+ CMD wget --spider -q http://localhost:8080/api/healthz
-ENTRYPOINT ["/app/glance"]
+EXPOSE 8080/tcp
+ENTRYPOINT ["/app/glance", "--config", "/app/config/glance.yml"]
diff --git a/README.md b/README.md
index 0e8cfb4..da6fb58 100644
--- a/README.md
+++ b/README.md
@@ -20,6 +20,7 @@
* Twitch channels & top games
* GitHub releases
* Repository overview
+* Docker containers
* Site monitor
* Search box
@@ -51,6 +52,8 @@ Checkout the [releases page](https://github.com/glanceapp/glance/releases) for a
```
#### Docker
+
+
> [!IMPORTANT]
>
> Make sure you have a valid `glance.yml` file in the same directory before running the container.
diff --git a/docs/configuration.md b/docs/configuration.md
index 6f9d602..c818e52 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -15,6 +15,8 @@
- [Reddit](#reddit)
- [Search](#search-widget)
- [Group](#group)
+ - [Split Column](#split-column)
+ - [Custom API](#custom-api)
- [Extension](#extension)
- [Weather](#weather)
- [Monitor](#monitor)
@@ -30,8 +32,10 @@
- [Twitch Top Games](#twitch-top-games)
- [iframe](#iframe)
- [HTML](#html)
+ - [Docker](#docker)
## Intro
+
Configuration is done via a single YAML file and a server restart is required in order for any changes to take effect. Trying to start the server with an invalid config file will result in an error.
## Preconfigured page
@@ -111,6 +115,8 @@ This will give you a page that looks like the following:
Configure the widgets, add more of them, add extra pages, etc. Make it your own!
+
+
## Server
Server configuration is done through a top level `server` property. Example:
@@ -281,6 +287,7 @@ theme:
> .widget-type-rss a {
> font-size: 1.5rem;
> }
+> ```
>
> In addition, you can also use the `css-class` property which is available on every widget to set custom class names for individual widgets.
@@ -313,6 +320,7 @@ pages:
| width | string | no | |
| center-vertically | boolean | no | false |
| hide-desktop-navigation | boolean | no | false |
+| expand-mobile-page-navigation | boolean | no | false |
| show-mobile-header | boolean | no | false |
| columns | array | yes | |
@@ -339,6 +347,9 @@ When set to `true`, vertically centers the content on the page. Has no effect if
#### `hide-desktop-navigation`
Whether to show the navigation links at the top of the page on desktop.
+#### `expand-mobile-page-navigation`
+Whether the mobile page navigation should be expanded by default.
+
#### `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.
@@ -525,10 +536,22 @@ An array of RSS/atom feeds. The title can optionally be changed.
| hide-categories | boolean | no | false | Only applicable for `detailed-list` style |
| hide-description | boolean | no | false | Only applicable for `detailed-list` style |
| item-link-prefix | string | no | | |
+| headers | key (string) & value (string) | no | | |
###### `item-link-prefix`
If an RSS feed isn't returning item links with a base domain and Glance has failed to automatically detect the correct domain you can manually add a prefix to each link with this property.
+###### `headers`
+Optionally specify the headers that will be sent with the request. Example:
+
+```yaml
+- type: rss
+ feeds:
+ - url: https://domain.com/rss
+ headers:
+ User-Agent: Custom User Agent
+```
+
##### `limit`
The maximum number of articles to show.
@@ -565,7 +588,15 @@ Preview:
| video-url-template | string | no | https://www.youtube.com/watch?v={VIDEO-ID} |
##### `channels`
-A list of channel IDs. One way of getting the ID of a channel is going to the channel's page and clicking on its description:
+A list of channel or playlist IDs. To specify a playlist, use the `playlist:` prefix like such:
+
+```yaml
+channels:
+ - playlist:PL8mG-RkN2uTyZZ00ObwZxxoG_nJbs3qec
+ - playlist:PL8mG-RkN2uTxTK4m_Vl2dYR9yE41kRdBg
+```
+
+One way of getting the ID of a channel is going to the channel's page and clicking on its description:

@@ -828,6 +859,7 @@ Preview:
| Enter | Perform search in the same tab | Search input is focused and not empty |
| Ctrl + Enter | Perform search in a new tab | Search input is focused and not empty |
| Escape | Leave focus | Search input is focused |
+| Up | Insert the last search query since the page was opened into the input field | Search input is focused |
> [!TIP]
>
@@ -890,7 +922,7 @@ url: https://www.amazon.com/s?k={QUERY}
```
### Group
-Group multiple widgets into one using tabs. Widgets are defined using a `widgets` property exactly as you would on a page column. The only limitation is that you cannot place a group widget within a group widget.
+Group multiple widgets into one using tabs. Widgets are defined using a `widgets` property exactly as you would on a page column. The only limitation is that you cannot place a group widget or a split column widget within a group widget.
Example:
@@ -933,6 +965,67 @@ Example:
<<: *shared-properties
```
+### Split Column
+
+Splits a full sized column in half, allowing you to place widgets side by side. This is converted to a single column on mobile devices or if not enough width is available. Widgets are defined using a `widgets` property exactly as you would on a page column.
+
+Example of a full page with an effective 4 column layout using two split column widgets inside of two full sized columns:
+
+
+View config
+
+```yaml
+shared:
+ - &reddit-props
+ type: reddit
+ collapse-after: 4
+ show-thumbnails: true
+
+pages:
+ - name: Split Column Demo
+ width: wide
+ columns:
+ - size: full
+ widgets:
+ - type: split-column
+ widgets:
+ - subreddit: gaming
+ <<: *reddit-props
+ - subreddit: worldnews
+ <<: *reddit-props
+ - subreddit: lifeprotips
+ <<: *reddit-props
+ show-thumbnails: false
+ - subreddit: askreddit
+ <<: *reddit-props
+ show-thumbnails: false
+
+ - size: full
+ widgets:
+ - type: split-column
+ widgets:
+ - subreddit: todayilearned
+ <<: *reddit-props
+ collapse-after: 2
+ - subreddit: aww
+ <<: *reddit-props
+ - subreddit: science
+ <<: *reddit-props
+ - subreddit: showerthoughts
+ <<: *reddit-props
+ show-thumbnails: false
+```
+
+
+
+
+Preview:
+
+
+
+### Custom API
+
+
### Extension
Display a widget provided by an external source (3rd party). If you want to learn more about developing extensions, checkout the [extensions documentation](extensions.md) (WIP).
@@ -948,11 +1041,15 @@ Display a widget provided by an external source (3rd party). If you want to lear
| Name | Type | Required | Default |
| ---- | ---- | -------- | ------- |
| url | string | yes | |
+| fallback-content-type | string | no | |
| allow-potentially-dangerous-html | boolean | no | false |
| parameters | key & value | no | |
##### `url`
-The URL of the extension.
+The URL of the extension. **Note that the query gets stripped from this URL and the one defined by `parameters` gets used instead.**
+
+##### `fallback-content-type`
+Optionally specify the fallback content type of the extension if the URL does not return a valid `Widget-Content-Type` header. Currently the only supported value for this property is `html`.
##### `allow-potentially-dangerous-html`
Whether to allow the extension to display HTML.
@@ -1065,11 +1162,19 @@ You can hover over the "ERROR" text to view more information.
| Name | Type | Required | Default |
| ---- | ---- | -------- | ------- |
| sites | array | yes | |
+| style | string | no | |
| show-failing-only | boolean | no | false |
##### `show-failing-only`
Shows only a list of failing sites when set to `true`.
+##### `style`
+Used to change the appearance of the widget. Possible values are `compact`.
+
+Preview of `compact`:
+
+
+
##### `sites`
Properties for each site:
@@ -1082,6 +1187,7 @@ Properties for each site:
| icon | string | no | |
| allow-insecure | boolean | no | false |
| same-tab | boolean | no | false |
+| alt-status-codes | array | no | |
`title`
@@ -1097,7 +1203,7 @@ The URL which will be requested and its response will determine the status of th
`icon`
-Optional URL to an image which will be used as the icon for the site. Can be an external URL or internal via [server configured assets](#assets-path). You can also directly use [Simple Icons](https://simpleicons.org/) via a `si:` prefix:
+Optional URL to an image which will be used as the icon for the site. Can be an external URL or internal via [server configured assets](#assets-path). You can also directly use [Simple Icons](https://simpleicons.org/) via a `si:` prefix or [Dashboard Icons](https://github.com/walkxcode/dashboard-icons) via a `di:` prefix:
```yaml
icon: si:jellyfin
@@ -1107,7 +1213,7 @@ icon: si:adguard
> [!WARNING]
>
-> Simple Icons are loaded externally and are hosted on `cdnjs.cloudflare.com`, if you do not wish to depend on a 3rd party you are free to download the icons individually and host them locally.
+> Simple Icons are loaded externally and are hosted on `cdn.jsdelivr.net`, if you do not wish to depend on a 3rd party you are free to download the icons individually and host them locally.
`allow-insecure`
@@ -1117,6 +1223,15 @@ Whether to ignore invalid/self-signed certificates.
Whether to open the link in the same or a new tab.
+`alt-status-codes`
+
+Status codes other than 200 that you want to return "OK".
+
+```yaml
+alt-status-codes:
+ - 403
+```
+
### Releases
Display a list of latest releases for specific repositories on Github, GitLab, Codeberg or Docker Hub.
@@ -1238,15 +1353,21 @@ Preview:
| Name | Type | Required | Default |
| ---- | ---- | -------- | ------- |
| service | string | no | pihole |
+| allow-insecure | bool | no | false |
| url | string | yes | |
| username | string | when service is `adguard` | |
| password | string | when service is `adguard` | |
| token | string | when service is `pihole` | |
+| hide-graph | bool | no | false |
+| hide-top-domains | bool | no | false |
| hour-format | string | no | 12h |
##### `service`
Either `adguard` or `pihole`.
+##### `allow-insecure`
+Whether to allow invalid/self-signed certificates when making the request to the service.
+
##### `url`
The base URL of the service. Can be specified from an environment variable using the syntax `${VARIABLE_NAME}`.
@@ -1259,6 +1380,12 @@ Only required when using AdGuard Home. The password used to log into the admin d
##### `token`
Only required when using Pi-hole. The API token which can be found in `Settings -> API -> Show API token`. Can be specified from an environment variable using the syntax `${VARIABLE_NAME}`.
+##### `hide-graph`
+Whether to hide the graph showing the number of queries over time.
+
+##### `hide-top-domains`
+Whether to hide the list of top blocked domains.
+
##### `hour-format`
Whether to display the relative time in the graph in `12h` or `24h` format.
@@ -1363,6 +1490,12 @@ An array of groups which can optionally have a title and a custom color.
| title | string | no | |
| color | HSL | no | the primary color of the theme |
| links | array | yes | |
+| same-tab | boolean | no | false |
+| hide-arrow | boolean | no | false |
+
+> [!TIP]
+>
+> You can set `same-tab` and `hide-arrow` either on the group which will apply them to all links in that group, or on each individual link which will override the value set on the group.
###### Properties for each link
| Name | Type | Required | Default |
@@ -1375,7 +1508,7 @@ An array of groups which can optionally have a title and a custom color.
`icon`
-URL pointing to an image. You can also directly use [Simple Icons](https://simpleicons.org/) via a `si:` prefix:
+URL pointing to an image. You can also directly use [Simple Icons](https://simpleicons.org/) via a `si:` prefix or [Dashboard Icons](https://github.com/walkxcode/dashboard-icons) via a `di:` prefix:
```yaml
icon: si:gmail
@@ -1385,7 +1518,7 @@ icon: si:reddit
> [!WARNING]
>
-> Simple Icons are loaded externally and are hosted on `cdnjs.cloudflare.com`, if you do not wish to depend on a 3rd party you are free to download the icons individually and host them locally.
+> Simple Icons are loaded externally and are hosted on `cdn.jsdelivr.net`, if you do not wish to depend on a 3rd party you are free to download the icons individually and host them locally.
`same-tab`
@@ -1494,15 +1627,25 @@ Example:
```yaml
- type: calendar
+ start-sunday: false
```
Preview:

+#### Properties
+
+| Name | Type | Required | Default |
+| ---- | ---- | -------- | ------- |
+| start-sunday | boolean | no | false |
+
+##### `start-sunday`
+Whether calendar weeks start on Sunday or Monday.
+
> [!NOTE]
>
-> There is currently no customizability available for the calendar. Extra features will be added in the future.
+> There is currently little customizability available for the calendar. Extra features will be added in the future.
### Markets
Display a list of markets, their current value, change for the day and a small 21d chart. Data is taken from Yahoo Finance.
@@ -1539,7 +1682,7 @@ Preview:
An array of markets for which to display information about.
##### `sort-by`
-By default the markets 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.
+By default the markets are displayed in the order they were defined. You can customize their ordering by setting the `sort-by` property to `change` for descending order based on the stock's percentage change (e.g. 1% would be sorted higher than -1%) or `absolute-change` for descending order based on the stock's absolute price change (e.g. -1% would be sorted higher than +0.5%).
###### Properties for each stock
| Name | Type | Required |
@@ -1674,3 +1817,75 @@ Example:
```
Note the use of `|` after `source:`, this allows you to insert a multi-line string.
+
+### Docker Containers
+
+The Docker widget allows you to monitor your Docker containers.
+To enable this feature, ensure that your setup provides access to the **docker.sock** file (also you may use a TCP connection).
+
+Add the following to your `docker-compose` or `docker run` command to enable the Docker widget:
+
+**Docker Example:**
+```bash
+docker run -d -p 8080:8080 \
+ -v ./glance.yml:/app/glance.yml \
+ -v /etc/timezone:/etc/timezone:ro \
+ -v /etc/localtime:/etc/localtime:ro \
+ -v /var/run/docker.sock:/var/run/docker.sock:ro \
+ glanceapp/glance
+```
+
+**Docker Compose Example:**
+```yaml
+services:
+ glance:
+ image: glanceapp/glance
+ volumes:
+ - ./glance.yml:/app/glance.yml
+ - /etc/timezone:/etc/timezone:ro
+ - /etc/localtime:/etc/localtime:ro
+ - /var/run/docker.sock:/var/run/docker.sock:ro
+ ports:
+ - 8080:8080
+ restart: unless-stopped
+```
+
+#### Configuration
+To integrate the Docker widget into your dashboard, include the following snippet in your `glance.yml` file:
+
+```yaml
+- type: docker
+ host-url: tcp://localhost:2375
+ cache: 1m
+```
+
+#### Properties
+
+| Name | Type | Required | Default |
+| ---- | ---- | -------- | ------- |
+| host-url | string | no | `unix:///var/run/docker.sock` |
+
+#### Leveraging Container Labels
+You can use container labels to control visibility, URLs, icons, and titles within the Docker widget. Add the following labels to your container configuration for enhanced customization:
+
+```yaml
+labels:
+ - "glance.enable=true" # Enable or disable visibility of the container (default: true)
+ - "glance.title=Glance" # Optional friendly name (defaults to container name)
+ - "glance.url=https://app.example.com" # Optional URL associated with the container
+ - "glance.iconUrl=si:docker" # Optional URL to an image which will be used as the icon for the site
+
+```
+
+**Default Values:**
+
+| Name | Default |
+|----------------|------------|
+| glance.enable | true |
+| glance.title | Container name |
+| glance.url | (none) |
+| glance.iconUrl | si:docker |
+
+Preview:
+
+
diff --git a/docs/extensions.md b/docs/extensions.md
index 06db1ae..b1fa4fa 100644
--- a/docs/extensions.md
+++ b/docs/extensions.md
@@ -29,6 +29,9 @@ Used to specify the title of the widget. If not provided, the widget's title wil
### `Widget-Content-Type`
Used to specify the content type that will be returned by the extension. If not provided, the content will be shown as plain text.
+### `Widget-Content-Frameless`
+When set to `true`, the widget's content will be displayed without the default background or "frame".
+
## Content Types
> [!NOTE]
diff --git a/docs/images/docker-widget-preview.png b/docs/images/docker-widget-preview.png
new file mode 100644
index 0000000..5b644d4
Binary files /dev/null and b/docs/images/docker-widget-preview.png differ
diff --git a/docs/images/monitor-widget-compact-preview.png b/docs/images/monitor-widget-compact-preview.png
new file mode 100644
index 0000000..3e81fce
Binary files /dev/null and b/docs/images/monitor-widget-compact-preview.png differ
diff --git a/docs/images/split-column-widget-preview.png b/docs/images/split-column-widget-preview.png
new file mode 100644
index 0000000..f1931f8
Binary files /dev/null and b/docs/images/split-column-widget-preview.png differ
diff --git a/docs/preconfigured-pages.md b/docs/preconfigured-pages.md
index b382917..b70610b 100644
--- a/docs/preconfigured-pages.md
+++ b/docs/preconfigured-pages.md
@@ -4,6 +4,10 @@ Don't want to spend time configuring pages from scratch? No problem! Simply copy
Pull requests with your page configurations are welcome!
+> [!NOTE]
+>
+> Pages must be placed under a top level `pages:` key, you can read more about that [here](configuration.md#pages).
+
## Startpage

diff --git a/go.mod b/go.mod
index 9bde4c5..8517954 100644
--- a/go.mod
+++ b/go.mod
@@ -1,20 +1,25 @@
module github.com/glanceapp/glance
-go 1.22.5
+go 1.23.1
require (
+ github.com/fsnotify/fsnotify v1.8.0
github.com/mmcdole/gofeed v1.3.0
- golang.org/x/text v0.16.0
+ github.com/tidwall/gjson v1.18.0
+ golang.org/x/text v0.21.0
gopkg.in/yaml.v3 v3.0.1
)
require (
- github.com/PuerkitoBio/goquery v1.9.2 // indirect
- github.com/andybalholm/cascadia v1.3.2 // indirect
github.com/arran4/golang-ical v0.3.1 // indirect
+ github.com/PuerkitoBio/goquery v1.10.0 // indirect
+ github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/mmcdole/goxpp v1.1.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
- golang.org/x/net v0.27.0 // indirect
+ github.com/tidwall/match v1.1.1 // indirect
+ github.com/tidwall/pretty v1.2.1 // indirect
+ golang.org/x/net v0.33.0 // indirect
+ golang.org/x/sys v0.28.0 // indirect
)
diff --git a/go.sum b/go.sum
index b79761a..593f29d 100644
--- a/go.sum
+++ b/go.sum
@@ -1,13 +1,15 @@
-github.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4yPeE=
-github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk=
-github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
-github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
+github.com/PuerkitoBio/goquery v1.10.0 h1:6fiXdLuUvYs2OJSvNRqlNPoBm6YABE226xrbavY5Wv4=
+github.com/PuerkitoBio/goquery v1.10.0/go.mod h1:TjZZl68Q3eGHNBA8CWaxAN7rOU1EbDz3CWuolcO5Yu4=
github.com/arran4/golang-ical v0.3.1 h1:v13B3eQZ9VDHTAvT6M11vVzxYgcYmjyPBE2eAZl3VZk=
github.com/arran4/golang-ical v0.3.1/go.mod h1:LZWxF8ZIu/sjBVUCV0udiVPrQAgq3V0aa0RfbO99Qkk=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
+github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
+github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
@@ -32,21 +34,45 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
+github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
+github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
+github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
+github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
+github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
+github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
+golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
+golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
+golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
-golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
-golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
+golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
+golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
+golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
+golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
+golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
+golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
+golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
+golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
+golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -54,21 +80,42 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
+golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
+golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
+golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
+golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
+golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
+golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
+golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
-golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
-golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
+golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
+golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
+golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
+golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
+golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
+golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/internal/assets/files.go b/internal/assets/files.go
deleted file mode 100644
index 2c7c09e..0000000
--- a/internal/assets/files.go
+++ /dev/null
@@ -1,56 +0,0 @@
-package assets
-
-import (
- "crypto/md5"
- "embed"
- "encoding/hex"
- "io"
- "io/fs"
- "log/slog"
- "strconv"
- "time"
-)
-
-//go:embed static
-var _publicFS embed.FS
-
-//go:embed templates
-var _templateFS embed.FS
-
-var PublicFS, _ = fs.Sub(_publicFS, "static")
-var TemplateFS, _ = fs.Sub(_templateFS, "templates")
-
-func getFSHash(files fs.FS) string {
- hash := md5.New()
-
- err := fs.WalkDir(files, ".", func(path string, d fs.DirEntry, err error) error {
- if err != nil {
- return err
- }
-
- if d.IsDir() {
- return nil
- }
-
- file, err := files.Open(path)
-
- if err != nil {
- return err
- }
-
- if _, err := io.Copy(hash, file); err != nil {
- return err
- }
-
- return nil
- })
-
- if err == nil {
- return hex.EncodeToString(hash.Sum(nil))[:10]
- }
-
- slog.Warn("Could not compute assets cache", "err", err)
- return strconv.FormatInt(time.Now().Unix(), 10)
-}
-
-var PublicFSHash = getFSHash(PublicFS)
diff --git a/internal/assets/templates.go b/internal/assets/templates.go
deleted file mode 100644
index 85abb69..0000000
--- a/internal/assets/templates.go
+++ /dev/null
@@ -1,109 +0,0 @@
-package assets
-
-import (
- "fmt"
- "html/template"
- "math"
- "strconv"
- "time"
-
- "golang.org/x/text/language"
- "golang.org/x/text/message"
-)
-
-var (
- PageTemplate = compileTemplate("page.html", "document.html", "page-style-overrides.gotmpl")
- PageContentTemplate = compileTemplate("content.html")
- CalendarTemplate = compileTemplate("calendar.html", "widget-base.html")
- ClockTemplate = compileTemplate("clock.html", "widget-base.html")
- BookmarksTemplate = compileTemplate("bookmarks.html", "widget-base.html")
- IFrameTemplate = compileTemplate("iframe.html", "widget-base.html")
- WeatherTemplate = compileTemplate("weather.html", "widget-base.html")
- ForumPostsTemplate = compileTemplate("forum-posts.html", "widget-base.html")
- 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")
- ChangeDetectionTemplate = compileTemplate("change-detection.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")
- MarketsTemplate = compileTemplate("markets.html", "widget-base.html")
- RSSListTemplate = compileTemplate("rss-list.html", "widget-base.html")
- RSSDetailedListTemplate = compileTemplate("rss-detailed-list.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")
- SearchTemplate = compileTemplate("search.html", "widget-base.html")
- ExtensionTemplate = compileTemplate("extension.html", "widget-base.html")
- GroupTemplate = compileTemplate("group.html", "widget-base.html")
- DNSStatsTemplate = compileTemplate("dns-stats.html", "widget-base.html")
-)
-
-var globalTemplateFunctions = template.FuncMap{
- "relativeTime": relativeTimeSince,
- "formatViewerCount": formatViewerCount,
- "formatNumber": intl.Sprint,
- "absInt": func(i int) int {
- return int(math.Abs(float64(i)))
- },
- "formatPrice": func(price float64) string {
- return intl.Sprintf("%.2f", price)
- },
- "dynamicRelativeTimeAttrs": func(t time.Time) template.HTMLAttr {
- return template.HTMLAttr(fmt.Sprintf(`data-dynamic-relative-time="%d"`, t.Unix()))
- },
-}
-
-func compileTemplate(primary string, dependencies ...string) *template.Template {
- t, err := template.New(primary).
- Funcs(globalTemplateFunctions).
- ParseFS(TemplateFS, append([]string{primary}, dependencies...)...)
-
- if err != nil {
- panic(err)
- }
-
- return t
-}
-
-var intl = message.NewPrinter(language.English)
-
-func formatViewerCount(count int) string {
- if count < 1_000 {
- return strconv.Itoa(count)
- }
-
- if count < 10_000 {
- return fmt.Sprintf("%.1fk", float64(count)/1_000)
- }
-
- if count < 1_000_000 {
- return fmt.Sprintf("%dk", count/1_000)
- }
-
- return fmt.Sprintf("%.1fm", float64(count)/1_000_000)
-}
-
-func relativeTimeSince(t time.Time) string {
- delta := time.Since(t)
-
- if delta < time.Minute {
- return "1m"
- }
- if delta < time.Hour {
- return fmt.Sprintf("%dm", delta/time.Minute)
- }
- if delta < 24*time.Hour {
- return fmt.Sprintf("%dh", delta/time.Hour)
- }
- if delta < 30*24*time.Hour {
- return fmt.Sprintf("%dd", delta/(24*time.Hour))
- }
- if delta < 12*30*24*time.Hour {
- return fmt.Sprintf("%dmo", delta/(30*24*time.Hour))
- }
-
- return fmt.Sprintf("%dy", delta/(365*24*time.Hour))
-}
diff --git a/internal/assets/templates/extension.html b/internal/assets/templates/extension.html
deleted file mode 100644
index e5794c8..0000000
--- a/internal/assets/templates/extension.html
+++ /dev/null
@@ -1,5 +0,0 @@
-{{ template "widget-base.html" . }}
-
-{{ define "widget-content" }}
-{{ .Extension.Content }}
-{{ end }}
diff --git a/internal/assets/templates/page-style-overrides.gotmpl b/internal/assets/templates/page-style-overrides.gotmpl
deleted file mode 100644
index 0bf2a99..0000000
--- a/internal/assets/templates/page-style-overrides.gotmpl
+++ /dev/null
@@ -1,14 +0,0 @@
-
diff --git a/internal/assets/templates/repository.html b/internal/assets/templates/repository.html
deleted file mode 100644
index 53b6617..0000000
--- a/internal/assets/templates/repository.html
+++ /dev/null
@@ -1,61 +0,0 @@
-{{ template "widget-base.html" . }}
-
-{{ define "widget-content" }}
-{{ .RepositoryDetails.Name }}
-
+{{ end }}
+{{ end }}
diff --git a/internal/assets/templates/monitor.html b/internal/glance/templates/monitor.html
similarity index 62%
rename from internal/assets/templates/monitor.html
rename to internal/glance/templates/monitor.html
index b19f0e2..7e95b99 100644
--- a/internal/assets/templates/monitor.html
+++ b/internal/glance/templates/monitor.html
@@ -21,11 +21,11 @@
{{ end }}
{{ define "site" }}
-{{ if .IconUrl }}
-
+{{ if .Icon.URL }}
+
{{ end }}
{{ end }}
diff --git a/internal/assets/templates/content.html b/internal/glance/templates/page-content.html
similarity index 100%
rename from internal/assets/templates/content.html
rename to internal/glance/templates/page-content.html
diff --git a/internal/assets/templates/page.html b/internal/glance/templates/page.html
similarity index 89%
rename from internal/assets/templates/page.html
rename to internal/glance/templates/page.html
index d2cee76..e740d03 100644
--- a/internal/assets/templates/page.html
+++ b/internal/glance/templates/page.html
@@ -14,10 +14,13 @@
{{ define "document-root-attrs" }}class="{{ if .App.Config.Theme.Light }}light-scheme {{ end }}{{ if ne "" .Page.Width }}page-width-{{ .Page.Width }} {{ end }}{{ if .Page.CenterVertically }}page-center-vertically{{ end }}"{{ end }}
{{ define "document-head-after" }}
-{{ template "page-style-overrides.gotmpl" . }}
+{{ .App.ParsedThemeStyle }}
+
{{ if ne "" .App.Config.Theme.CustomCSSFile }}
{{ end }}
+
+{{ if ne "" .App.Config.Document.Head }}{{ .App.Config.Document.Head }}{{ end }}
{{ end }}
{{ define "navigation-links" }}
@@ -27,7 +30,7 @@
{{ end }}
{{ define "document-body" }}
-
+
{{ if not .Page.HideDesktopNavigation }}
@@ -44,9 +47,9 @@
↑
{{ range $i, $column := .Page.Columns }}
-
+
{{ end }}
-
+
{{ template "navigation-links" . }}
diff --git a/internal/assets/templates/reddit-horizontal-cards.html b/internal/glance/templates/reddit-horizontal-cards.html
similarity index 86%
rename from internal/assets/templates/reddit-horizontal-cards.html
rename to internal/glance/templates/reddit-horizontal-cards.html
index 2012435..6961216 100644
--- a/internal/assets/templates/reddit-horizontal-cards.html
+++ b/internal/glance/templates/reddit-horizontal-cards.html
@@ -18,7 +18,7 @@
{{ else }}
+{{ end }}
+
+{{ end }}
diff --git a/internal/assets/templates/rss-detailed-list.html b/internal/glance/templates/rss-detailed-list.html
similarity index 100%
rename from internal/assets/templates/rss-detailed-list.html
rename to internal/glance/templates/rss-detailed-list.html
diff --git a/internal/assets/templates/rss-horizontal-cards-2.html b/internal/glance/templates/rss-horizontal-cards-2.html
similarity index 91%
rename from internal/assets/templates/rss-horizontal-cards-2.html
rename to internal/glance/templates/rss-horizontal-cards-2.html
index 0404fce..496e56a 100644
--- a/internal/assets/templates/rss-horizontal-cards-2.html
+++ b/internal/glance/templates/rss-horizontal-cards-2.html
@@ -16,7 +16,7 @@
{{ end }}
diff --git a/internal/assets/templates/search.html b/internal/glance/templates/search.html
similarity index 100%
rename from internal/assets/templates/search.html
rename to internal/glance/templates/search.html
diff --git a/internal/glance/templates/split-column.html b/internal/glance/templates/split-column.html
new file mode 100644
index 0000000..63b4aea
--- /dev/null
+++ b/internal/glance/templates/split-column.html
@@ -0,0 +1,11 @@
+{{ template "widget-base.html" . }}
+
+{{ define "widget-content-classes" }}widget-content-frameless{{ end }}
+
+{{ define "widget-content" }}
+
+{{ range .Widgets }}
+ {{ .Render }}
+{{ end }}
+
+{{ end }}
diff --git a/internal/glance/templates/theme-style.gotmpl b/internal/glance/templates/theme-style.gotmpl
new file mode 100644
index 0000000..878ca0b
--- /dev/null
+++ b/internal/glance/templates/theme-style.gotmpl
@@ -0,0 +1,14 @@
+
diff --git a/internal/assets/templates/twitch-channels.html b/internal/glance/templates/twitch-channels.html
similarity index 75%
rename from internal/assets/templates/twitch-channels.html
rename to internal/glance/templates/twitch-channels.html
index aba7935..30529c5 100644
--- a/internal/assets/templates/twitch-channels.html
+++ b/internal/glance/templates/twitch-channels.html
@@ -5,9 +5,15 @@
{{ range .Channels }}
-
+
+ {{ if .IsLive }}
+
+
+
{{ .StreamTitle }}
+
+ {{ end }}
{{ if .Exists }}
-
+
{{ else }}
diff --git a/internal/assets/templates/twitch-games-list.html b/internal/glance/templates/twitch-games-list.html
similarity index 100%
rename from internal/assets/templates/twitch-games-list.html
rename to internal/glance/templates/twitch-games-list.html
diff --git a/internal/glance/templates/v0.7-update-notice-page.html b/internal/glance/templates/v0.7-update-notice-page.html
new file mode 100644
index 0000000..1f3f524
--- /dev/null
+++ b/internal/glance/templates/v0.7-update-notice-page.html
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+ Update notice
+
+
+
+
+
+
+
+
UPDATE NOTICE
+
+
+ The default location of glance.yml in the Docker image has
+ changed since v0.7.0, please see the migration guide
+ for instructions or visit the release notes
+ to find out more about why this change was necessary. Sorry for the inconvenience.
+
+
+
Migration should take around 5 minutes.
+
+
+
+
+
diff --git a/internal/assets/templates/video-card-contents.html b/internal/glance/templates/video-card-contents.html
similarity index 78%
rename from internal/assets/templates/video-card-contents.html
rename to internal/glance/templates/video-card-contents.html
index 375fd08..c6340c5 100644
--- a/internal/assets/templates/video-card-contents.html
+++ b/internal/glance/templates/video-card-contents.html
@@ -1,7 +1,7 @@
{{ define "video-card-contents" }}
diff --git a/internal/assets/templates/videos-grid.html b/internal/glance/templates/videos-grid.html
similarity index 100%
rename from internal/assets/templates/videos-grid.html
rename to internal/glance/templates/videos-grid.html
diff --git a/internal/assets/templates/videos.html b/internal/glance/templates/videos.html
similarity index 100%
rename from internal/assets/templates/videos.html
rename to internal/glance/templates/videos.html
diff --git a/internal/assets/templates/weather.html b/internal/glance/templates/weather.html
similarity index 100%
rename from internal/assets/templates/weather.html
rename to internal/glance/templates/weather.html
diff --git a/internal/assets/templates/widget-base.html b/internal/glance/templates/widget-base.html
similarity index 60%
rename from internal/assets/templates/widget-base.html
rename to internal/glance/templates/widget-base.html
index bdd30b9..0a8e3f2 100644
--- a/internal/assets/templates/widget-base.html
+++ b/internal/glance/templates/widget-base.html
@@ -1,7 +1,7 @@