mirror of
https://github.com/glanceapp/glance.git
synced 2025-06-21 18:31:24 +02:00
Merge remote-tracking branch 'upstream/release/v0.7.0' into alt-status-codes
Update to current release
This commit is contained in:
commit
04811a0658
@ -1,4 +1,4 @@
|
|||||||
FROM golang:1.22.5-alpine3.20 AS builder
|
FROM golang:1.23.1-alpine3.20 AS builder
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY . /app
|
COPY . /app
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
- [Reddit](#reddit)
|
- [Reddit](#reddit)
|
||||||
- [Search](#search-widget)
|
- [Search](#search-widget)
|
||||||
- [Group](#group)
|
- [Group](#group)
|
||||||
|
- [Split Column](#split-column)
|
||||||
- [Extension](#extension)
|
- [Extension](#extension)
|
||||||
- [Weather](#weather)
|
- [Weather](#weather)
|
||||||
- [Monitor](#monitor)
|
- [Monitor](#monitor)
|
||||||
@ -890,7 +891,7 @@ url: https://www.amazon.com/s?k={QUERY}
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Group
|
### 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:
|
Example:
|
||||||
|
|
||||||
@ -933,6 +934,63 @@ Example:
|
|||||||
<<: *shared-properties
|
<<: *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:
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>View config</summary>
|
||||||
|
|
||||||
|
```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
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
Preview:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
### Extension
|
### 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).
|
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,12 +1006,16 @@ Display a widget provided by an external source (3rd party). If you want to lear
|
|||||||
| Name | Type | Required | Default |
|
| Name | Type | Required | Default |
|
||||||
| ---- | ---- | -------- | ------- |
|
| ---- | ---- | -------- | ------- |
|
||||||
| url | string | yes | |
|
| url | string | yes | |
|
||||||
|
| fallback-content-type | string | no | |
|
||||||
| allow-potentially-dangerous-html | boolean | no | false |
|
| allow-potentially-dangerous-html | boolean | no | false |
|
||||||
| parameters | key & value | no | |
|
| parameters | key & value | no | |
|
||||||
|
|
||||||
##### `url`
|
##### `url`
|
||||||
The URL of the extension.
|
The URL of the extension.
|
||||||
|
|
||||||
|
##### `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`
|
##### `allow-potentially-dangerous-html`
|
||||||
Whether to allow the extension to display HTML.
|
Whether to allow the extension to display HTML.
|
||||||
|
|
||||||
@ -1108,7 +1170,7 @@ icon: si:adguard
|
|||||||
|
|
||||||
> [!WARNING]
|
> [!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`
|
`allow-insecure`
|
||||||
|
|
||||||
@ -1395,7 +1457,7 @@ icon: si:reddit
|
|||||||
|
|
||||||
> [!WARNING]
|
> [!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`
|
`same-tab`
|
||||||
|
|
||||||
@ -1549,7 +1611,7 @@ Preview:
|
|||||||
An array of markets for which to display information about.
|
An array of markets for which to display information about.
|
||||||
|
|
||||||
##### `sort-by`
|
##### `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
|
###### Properties for each stock
|
||||||
| Name | Type | Required |
|
| Name | Type | Required |
|
||||||
|
BIN
docs/images/split-column-widget-preview.png
Normal file
BIN
docs/images/split-column-widget-preview.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 328 KiB |
8
go.mod
8
go.mod
@ -1,19 +1,19 @@
|
|||||||
module github.com/glanceapp/glance
|
module github.com/glanceapp/glance
|
||||||
|
|
||||||
go 1.22.5
|
go 1.23.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/mmcdole/gofeed v1.3.0
|
github.com/mmcdole/gofeed v1.3.0
|
||||||
golang.org/x/text v0.16.0
|
golang.org/x/text v0.18.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/PuerkitoBio/goquery v1.9.2 // indirect
|
github.com/PuerkitoBio/goquery v1.10.0 // indirect
|
||||||
github.com/andybalholm/cascadia v1.3.2 // indirect
|
github.com/andybalholm/cascadia v1.3.2 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/mmcdole/goxpp v1.1.1 // indirect
|
github.com/mmcdole/goxpp v1.1.1 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
golang.org/x/net v0.27.0 // indirect
|
golang.org/x/net v0.29.0 // indirect
|
||||||
)
|
)
|
||||||
|
6
go.sum
6
go.sum
@ -1,5 +1,7 @@
|
|||||||
github.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4yPeE=
|
github.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4yPeE=
|
||||||
github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk=
|
github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk=
|
||||||
|
github.com/PuerkitoBio/goquery v1.10.0 h1:6fiXdLuUvYs2OJSvNRqlNPoBm6YABE226xrbavY5Wv4=
|
||||||
|
github.com/PuerkitoBio/goquery v1.10.0/go.mod h1:TjZZl68Q3eGHNBA8CWaxAN7rOU1EbDz3CWuolcO5Yu4=
|
||||||
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
|
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/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
@ -35,6 +37,8 @@ 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.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 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||||
|
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
|
||||||
|
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
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.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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
@ -56,6 +60,8 @@ 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.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 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||||
|
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
||||||
|
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { setupPopovers } from './popover.js';
|
import { setupPopovers } from './popover.js';
|
||||||
|
import { setupMasonries } from './masonry.js';
|
||||||
import { throttledDebounce, isElementVisible } from './utils.js';
|
import { throttledDebounce, isElementVisible } from './utils.js';
|
||||||
|
|
||||||
async function fetchPageContent(pageData) {
|
async function fetchPageContent(pageData) {
|
||||||
@ -581,6 +582,7 @@ async function setupPage() {
|
|||||||
setupCollapsibleLists();
|
setupCollapsibleLists();
|
||||||
setupCollapsibleGrids();
|
setupCollapsibleGrids();
|
||||||
setupGroups();
|
setupGroups();
|
||||||
|
setupMasonries();
|
||||||
setupDynamicRelativeTime();
|
setupDynamicRelativeTime();
|
||||||
setupLazyImages();
|
setupLazyImages();
|
||||||
} finally {
|
} finally {
|
||||||
|
53
internal/assets/static/js/masonry.js
Normal file
53
internal/assets/static/js/masonry.js
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
|
||||||
|
import { clamp } from "./utils.js";
|
||||||
|
|
||||||
|
export function setupMasonries() {
|
||||||
|
const masonryContainers = document.getElementsByClassName("masonry");
|
||||||
|
|
||||||
|
for (let i = 0; i < masonryContainers.length; i++) {
|
||||||
|
const container = masonryContainers[i];
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
minColumnWidth: container.dataset.minColumnWidth || 330,
|
||||||
|
maxColumns: container.dataset.maxColumns || 6,
|
||||||
|
};
|
||||||
|
|
||||||
|
const items = Array.from(container.children);
|
||||||
|
let previousColumnsCount = 0;
|
||||||
|
|
||||||
|
const render = function() {
|
||||||
|
const columnsCount = clamp(
|
||||||
|
Math.floor(container.offsetWidth / options.minColumnWidth),
|
||||||
|
1,
|
||||||
|
Math.min(options.maxColumns, items.length)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (columnsCount === previousColumnsCount) {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
container.textContent = "";
|
||||||
|
previousColumnsCount = columnsCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnsFragment = document.createDocumentFragment();
|
||||||
|
|
||||||
|
for (let i = 0; i < columnsCount; i++) {
|
||||||
|
const column = document.createElement("div");
|
||||||
|
column.className = "masonry-column";
|
||||||
|
columnsFragment.append(column);
|
||||||
|
}
|
||||||
|
|
||||||
|
// poor man's masonry
|
||||||
|
// TODO: add an option that allows placing items in the
|
||||||
|
// shortest column instead of iterating the columns in order
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
columnsFragment.children[i % columnsCount].appendChild(items[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
container.append(columnsFragment);
|
||||||
|
};
|
||||||
|
|
||||||
|
const observer = new ResizeObserver(() => requestAnimationFrame(render));
|
||||||
|
observer.observe(container);
|
||||||
|
}
|
||||||
|
}
|
@ -56,6 +56,8 @@ function clearTogglePopoverTimeout() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function showPopover() {
|
function showPopover() {
|
||||||
|
if (pendingTarget === null) return;
|
||||||
|
|
||||||
activeTarget = pendingTarget;
|
activeTarget = pendingTarget;
|
||||||
pendingTarget = null;
|
pendingTarget = null;
|
||||||
|
|
||||||
|
@ -23,3 +23,7 @@ export function throttledDebounce(callback, maxDebounceTimes, debounceDelay) {
|
|||||||
export function isElementVisible(element) {
|
export function isElementVisible(element) {
|
||||||
return !!(element.offsetWidth || element.offsetHeight || element.getClientRects().length);
|
return !!(element.offsetWidth || element.offsetHeight || element.getClientRects().length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function clamp(value, min, max) {
|
||||||
|
return Math.min(Math.max(value, min), max);
|
||||||
|
}
|
||||||
|
@ -440,6 +440,17 @@ kbd:active {
|
|||||||
box-shadow: 0 0 0 0 var(--color-widget-background-highlight);
|
box-shadow: 0 0 0 0 var(--color-widget-background-highlight);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.masonry {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--widget-gap);
|
||||||
|
}
|
||||||
|
|
||||||
|
.masonry-column {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
.popover-container, [data-popover-html] {
|
.popover-container, [data-popover-html] {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@ -1493,6 +1504,13 @@ details[open] .summary::after {
|
|||||||
border: 2px solid var(--color-widget-background);
|
border: 2px solid var(--color-widget-background);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.twitch-stream-preview {
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
.reddit-card-thumbnail {
|
.reddit-card-thumbnail {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -39,6 +39,7 @@ var (
|
|||||||
ExtensionTemplate = compileTemplate("extension.html", "widget-base.html")
|
ExtensionTemplate = compileTemplate("extension.html", "widget-base.html")
|
||||||
GroupTemplate = compileTemplate("group.html", "widget-base.html")
|
GroupTemplate = compileTemplate("group.html", "widget-base.html")
|
||||||
DNSStatsTemplate = compileTemplate("dns-stats.html", "widget-base.html")
|
DNSStatsTemplate = compileTemplate("dns-stats.html", "widget-base.html")
|
||||||
|
SplitColumnTemplate = compileTemplate("split-column.html", "widget-base.html")
|
||||||
)
|
)
|
||||||
|
|
||||||
var globalTemplateFunctions = template.FuncMap{
|
var globalTemplateFunctions = template.FuncMap{
|
||||||
|
@ -44,7 +44,7 @@
|
|||||||
<div class="mobile-navigation-icons">
|
<div class="mobile-navigation-icons">
|
||||||
<a class="mobile-navigation-label" href="#top">↑</a>
|
<a class="mobile-navigation-label" href="#top">↑</a>
|
||||||
{{ range $i, $column := .Page.Columns }}
|
{{ range $i, $column := .Page.Columns }}
|
||||||
<label class="mobile-navigation-label"><input type="radio" class="mobile-navigation-input" name="column" value="{{ $i }}" autocomplete="off"{{ if eq "full" $column.Size }} checked{{ end }}><div class="mobile-navigation-pill"></div></label>
|
<label class="mobile-navigation-label"><input type="radio" class="mobile-navigation-input" name="column" value="{{ $i }}" autocomplete="off"{{ if eq $i $.Page.PrimaryColumnIndex }} checked{{ end }}><div class="mobile-navigation-pill"></div></label>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
<label class="mobile-navigation-label"><input type="checkbox" class="mobile-navigation-page-links-input" autocomplete="on"><div class="hamburger-icon"></div></label>
|
<label class="mobile-navigation-label"><input type="checkbox" class="mobile-navigation-page-links-input" autocomplete="on"><div class="hamburger-icon"></div></label>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{{ template "widget-base.html" . }}
|
{{ template "widget-base.html" . }}
|
||||||
|
|
||||||
{{ define "widget-content" }}
|
{{ define "widget-content" }}
|
||||||
<ul class="list list-gap-10 collapsible-container single-line-titles" data-collapse-after="{{ .CollapseAfter }}">
|
<ul class="list list-gap-10 collapsible-container" data-collapse-after="{{ .CollapseAfter }}">
|
||||||
{{ range .Releases }}
|
{{ range .Releases }}
|
||||||
<li>
|
<li>
|
||||||
<div class="flex items-center gap-10">
|
<div class="flex items-center gap-10">
|
||||||
|
11
internal/assets/templates/split-column.html
Normal file
11
internal/assets/templates/split-column.html
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{{ template "widget-base.html" . }}
|
||||||
|
|
||||||
|
{{ define "widget-content-classes" }}widget-content-frameless{{ end }}
|
||||||
|
|
||||||
|
{{ define "widget-content" }}
|
||||||
|
<div class="masonry" data-max-columns="2">
|
||||||
|
{{ range .Widgets }}
|
||||||
|
{{ .Render }}
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
@ -5,7 +5,13 @@
|
|||||||
{{ range .Channels }}
|
{{ range .Channels }}
|
||||||
<li>
|
<li>
|
||||||
<div class="{{ if .IsLive }}twitch-channel-live {{ end }}flex gap-10 items-start thumbnail-parent">
|
<div class="{{ if .IsLive }}twitch-channel-live {{ end }}flex gap-10 items-start thumbnail-parent">
|
||||||
<div class="twitch-channel-avatar-container">
|
<div class="twitch-channel-avatar-container"{{ if .IsLive }} data-popover-type="html" data-popover-position="above" data-popover-margin="0.15rem"{{ end }}>
|
||||||
|
{{ if .IsLive }}
|
||||||
|
<div data-popover-html>
|
||||||
|
<img class="twitch-stream-preview" src="https://static-cdn.jtvnw.net/previews-ttv/live_user_{{ .Login }}-440x248.jpg" loading="lazy" alt="">
|
||||||
|
<p class="margin-top-10 color-highlight text-truncate-3-lines">{{ .StreamTitle }}</p>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
{{ if .Exists }}
|
{{ if .Exists }}
|
||||||
<a href="https://twitch.tv/{{ .Login }}" class="twitch-channel-avatar-link" target="_blank" rel="noreferrer">
|
<a href="https://twitch.tv/{{ .Login }}" class="twitch-channel-avatar-link" target="_blank" rel="noreferrer">
|
||||||
<img class="twitch-channel-avatar thumbnail" src="{{ .AvatarUrl }}" alt="" loading="lazy">
|
<img class="twitch-channel-avatar thumbnail" src="{{ .AvatarUrl }}" alt="" loading="lazy">
|
||||||
|
@ -28,6 +28,7 @@ const (
|
|||||||
|
|
||||||
type ExtensionRequestOptions struct {
|
type ExtensionRequestOptions struct {
|
||||||
URL string `yaml:"url"`
|
URL string `yaml:"url"`
|
||||||
|
FallbackContentType string `yaml:"fallback-content-type"`
|
||||||
Parameters map[string]string `yaml:"parameters"`
|
Parameters map[string]string `yaml:"parameters"`
|
||||||
AllowHtml bool `yaml:"allow-potentially-dangerous-html"`
|
AllowHtml bool `yaml:"allow-potentially-dangerous-html"`
|
||||||
}
|
}
|
||||||
@ -87,9 +88,13 @@ func FetchExtension(options ExtensionRequestOptions) (Extension, error) {
|
|||||||
|
|
||||||
contentType, ok := ExtensionStringToType[response.Header.Get(ExtensionHeaderContentType)]
|
contentType, ok := ExtensionStringToType[response.Header.Get(ExtensionHeaderContentType)]
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
contentType, ok = ExtensionStringToType[options.FallbackContentType]
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
contentType = ExtensionContentUnknown
|
contentType = ExtensionContentUnknown
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension.Content = convertExtensionContent(options, body, contentType)
|
extension.Content = convertExtensionContent(options, body, contentType)
|
||||||
|
|
||||||
|
@ -189,12 +189,19 @@ func FetchWeatherForPlace(place *PlaceJson, units string) (*Weather, error) {
|
|||||||
minT := slices.Min(temperatures)
|
minT := slices.Min(temperatures)
|
||||||
maxT := slices.Max(temperatures)
|
maxT := slices.Max(temperatures)
|
||||||
|
|
||||||
|
temperaturesRange := float64(maxT - minT)
|
||||||
|
|
||||||
for i := 0; i < 12; i++ {
|
for i := 0; i < 12; i++ {
|
||||||
bars = append(bars, weatherColumn{
|
bars = append(bars, weatherColumn{
|
||||||
Temperature: temperatures[i],
|
Temperature: temperatures[i],
|
||||||
Scale: float64(temperatures[i]-minT) / float64(maxT-minT),
|
|
||||||
HasPrecipitation: precipitations[i],
|
HasPrecipitation: precipitations[i],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if temperaturesRange > 0 {
|
||||||
|
bars[i].Scale = float64(temperatures[i]-minT) / temperaturesRange
|
||||||
|
} else {
|
||||||
|
bars[i].Scale = 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -133,6 +133,12 @@ func (t Markets) SortByAbsChange() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t Markets) SortByChange() {
|
||||||
|
sort.Slice(t, func(i, j int) bool {
|
||||||
|
return t[i].PercentChange > t[j].PercentChange
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
var weatherCodeTable = map[int]string{
|
var weatherCodeTable = map[int]string{
|
||||||
0: "Clear Sky",
|
0: "Clear Sky",
|
||||||
1: "Mainly Clear",
|
1: "Mainly Clear",
|
||||||
|
@ -28,6 +28,7 @@ type TwitchChannel struct {
|
|||||||
Login string
|
Login string
|
||||||
Exists bool
|
Exists bool
|
||||||
Name string
|
Name string
|
||||||
|
StreamTitle string
|
||||||
AvatarUrl string
|
AvatarUrl string
|
||||||
IsLive bool
|
IsLive bool
|
||||||
LiveSince time.Time
|
LiveSince time.Time
|
||||||
@ -77,6 +78,9 @@ type twitchStreamMetadataOperationResponse struct {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
} `json:"game"`
|
} `json:"game"`
|
||||||
} `json:"stream"`
|
} `json:"stream"`
|
||||||
|
LastBroadcast *struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
}
|
||||||
} `json:"user"`
|
} `json:"user"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -142,7 +146,10 @@ func FetchTopGamesFromTwitch(exclude []string, limit int) ([]TwitchCategory, err
|
|||||||
return categories, nil
|
return categories, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const twitchChannelStatusOperationRequestBody = `[{"operationName":"ChannelShell","variables":{"login":"%s"},"extensions":{"persistedQuery":{"version":1,"sha256Hash":"580ab410bcd0c1ad194224957ae2241e5d252b2c5173d8e0cce9d32d5bb14efe"}}},{"operationName":"StreamMetadata","variables":{"channelLogin":"%s"},"extensions":{"persistedQuery":{"version":1,"sha256Hash":"676ee2f834ede42eb4514cdb432b3134fefc12590080c9a2c9bb44a2a4a63266"}}}]`
|
const twitchChannelStatusOperationRequestBody = `[
|
||||||
|
{"operationName":"ChannelShell","variables":{"login":"%s"},"extensions":{"persistedQuery":{"version":1,"sha256Hash":"580ab410bcd0c1ad194224957ae2241e5d252b2c5173d8e0cce9d32d5bb14efe"}}},
|
||||||
|
{"operationName":"StreamMetadata","variables":{"channelLogin":"%s"},"extensions":{"persistedQuery":{"version":1,"sha256Hash":"676ee2f834ede42eb4514cdb432b3134fefc12590080c9a2c9bb44a2a4a63266"}}}
|
||||||
|
]`
|
||||||
|
|
||||||
// TODO: rework
|
// TODO: rework
|
||||||
// The operations for multiple channels can all be sent in a single request
|
// The operations for multiple channels can all be sent in a single request
|
||||||
@ -205,6 +212,10 @@ func fetchChannelFromTwitchTask(channel string) (TwitchChannel, error) {
|
|||||||
result.ViewersCount = channelShell.UserOrError.Stream.ViewersCount
|
result.ViewersCount = channelShell.UserOrError.Stream.ViewersCount
|
||||||
|
|
||||||
if streamMetadata.UserOrNull != nil && streamMetadata.UserOrNull.Stream != nil {
|
if streamMetadata.UserOrNull != nil && streamMetadata.UserOrNull.Stream != nil {
|
||||||
|
if streamMetadata.UserOrNull.LastBroadcast != nil {
|
||||||
|
result.StreamTitle = streamMetadata.UserOrNull.LastBroadcast.Title
|
||||||
|
}
|
||||||
|
|
||||||
if streamMetadata.UserOrNull.Stream.Game != nil {
|
if streamMetadata.UserOrNull.Stream.Game != nil {
|
||||||
result.Category = streamMetadata.UserOrNull.Stream.Game.Name
|
result.Category = streamMetadata.UserOrNull.Stream.Game.Name
|
||||||
result.CategorySlug = streamMetadata.UserOrNull.Stream.Game.Slug
|
result.CategorySlug = streamMetadata.UserOrNull.Stream.Game.Slug
|
||||||
|
@ -75,6 +75,7 @@ type Page struct {
|
|||||||
HideDesktopNavigation bool `yaml:"hide-desktop-navigation"`
|
HideDesktopNavigation bool `yaml:"hide-desktop-navigation"`
|
||||||
CenterVertically bool `yaml:"center-vertically"`
|
CenterVertically bool `yaml:"center-vertically"`
|
||||||
Columns []Column `yaml:"columns"`
|
Columns []Column `yaml:"columns"`
|
||||||
|
PrimaryColumnIndex int8 `yaml:"-"`
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -140,15 +141,24 @@ func NewApplication(config *Config) (*Application, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for p := range config.Pages {
|
for p := range config.Pages {
|
||||||
if config.Pages[p].Slug == "" {
|
page := &config.Pages[p]
|
||||||
config.Pages[p].Slug = titleToSlug(config.Pages[p].Title)
|
page.PrimaryColumnIndex = -1
|
||||||
|
|
||||||
|
if page.Slug == "" {
|
||||||
|
page.Slug = titleToSlug(page.Title)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.slugToPage[config.Pages[p].Slug] = &config.Pages[p]
|
app.slugToPage[page.Slug] = page
|
||||||
|
|
||||||
for c := range config.Pages[p].Columns {
|
for c := range page.Columns {
|
||||||
for w := range config.Pages[p].Columns[c].Widgets {
|
column := &page.Columns[c]
|
||||||
widget := config.Pages[p].Columns[c].Widgets[w]
|
|
||||||
|
if page.PrimaryColumnIndex == -1 && column.Size == "full" {
|
||||||
|
page.PrimaryColumnIndex = int8(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
for w := range column.Widgets {
|
||||||
|
widget := column.Widgets[w]
|
||||||
app.widgetByID[widget.GetID()] = widget
|
app.widgetByID[widget.GetID()] = widget
|
||||||
|
|
||||||
widget.SetProviders(providers)
|
widget.SetProviders(providers)
|
||||||
|
48
internal/widget/container.go
Normal file
48
internal/widget/container.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
package widget
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type containerWidgetBase struct {
|
||||||
|
Widgets Widgets `yaml:"widgets"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (widget *containerWidgetBase) Update(ctx context.Context) {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
for w := range widget.Widgets {
|
||||||
|
widget := widget.Widgets[w]
|
||||||
|
|
||||||
|
if !widget.RequiresUpdate(&now) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
widget.Update(ctx)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (widget *containerWidgetBase) SetProviders(providers *Providers) {
|
||||||
|
for i := range widget.Widgets {
|
||||||
|
widget.Widgets[i].SetProviders(providers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (widget *containerWidgetBase) RequiresUpdate(now *time.Time) bool {
|
||||||
|
for i := range widget.Widgets {
|
||||||
|
if widget.Widgets[i].RequiresUpdate(now) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
@ -14,6 +14,7 @@ import (
|
|||||||
type Extension struct {
|
type Extension struct {
|
||||||
widgetBase `yaml:",inline"`
|
widgetBase `yaml:",inline"`
|
||||||
URL string `yaml:"url"`
|
URL string `yaml:"url"`
|
||||||
|
FallbackContentType string `yaml:"fallback-content-type"`
|
||||||
Parameters map[string]string `yaml:"parameters"`
|
Parameters map[string]string `yaml:"parameters"`
|
||||||
AllowHtml bool `yaml:"allow-potentially-dangerous-html"`
|
AllowHtml bool `yaml:"allow-potentially-dangerous-html"`
|
||||||
Extension feed.Extension `yaml:"-"`
|
Extension feed.Extension `yaml:"-"`
|
||||||
@ -39,6 +40,7 @@ func (widget *Extension) Initialize() error {
|
|||||||
func (widget *Extension) Update(ctx context.Context) {
|
func (widget *Extension) Update(ctx context.Context) {
|
||||||
extension, err := feed.FetchExtension(feed.ExtensionRequestOptions{
|
extension, err := feed.FetchExtension(feed.ExtensionRequestOptions{
|
||||||
URL: widget.URL,
|
URL: widget.URL,
|
||||||
|
FallbackContentType: widget.FallbackContentType,
|
||||||
Parameters: widget.Parameters,
|
Parameters: widget.Parameters,
|
||||||
AllowHtml: widget.AllowHtml,
|
AllowHtml: widget.AllowHtml,
|
||||||
})
|
})
|
||||||
|
@ -13,7 +13,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var HSLColorPattern = regexp.MustCompile(`^(?:hsla?\()?(\d{1,3})(?: |,)+(\d{1,3})%?(?: |,)+(\d{1,3})%?\)?$`)
|
var HSLColorPattern = regexp.MustCompile(`^(?:hsla?\()?(\d{1,3})(?: |,)+(\d{1,3})%?(?: |,)+(\d{1,3})%?\)?$`)
|
||||||
var EnvFieldPattern = regexp.MustCompile(`^\${([A-Z_]+)}$`)
|
var EnvFieldPattern = regexp.MustCompile(`(^|.)\$\{([A-Z_]+)\}`)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
HSLHueMax = 360
|
HSLHueMax = 360
|
||||||
@ -133,21 +133,42 @@ func (f *OptionalEnvString) UnmarshalYAML(node *yaml.Node) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
matches := EnvFieldPattern.FindStringSubmatch(value)
|
replaced := EnvFieldPattern.ReplaceAllStringFunc(value, func(whole string) string {
|
||||||
|
if err != nil {
|
||||||
if len(matches) != 2 {
|
return ""
|
||||||
*f = OptionalEnvString(value)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
value, found := os.LookupEnv(matches[1])
|
groups := EnvFieldPattern.FindStringSubmatch(whole)
|
||||||
|
|
||||||
|
if len(groups) != 3 {
|
||||||
|
return whole
|
||||||
|
}
|
||||||
|
|
||||||
|
prefix, key := groups[1], groups[2]
|
||||||
|
|
||||||
|
if prefix == `\` {
|
||||||
|
if len(whole) >= 2 {
|
||||||
|
return whole[1:]
|
||||||
|
} else {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
value, found := os.LookupEnv(key)
|
||||||
|
|
||||||
if !found {
|
if !found {
|
||||||
return fmt.Errorf("environment variable %s not found", matches[1])
|
err = fmt.Errorf("environment variable %s not found", key)
|
||||||
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
*f = OptionalEnvString(value)
|
return prefix + value
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*f = OptionalEnvString(replaced)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -162,7 +183,7 @@ func toSimpleIconIfPrefixed(icon string) (string, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
icon = strings.TrimPrefix(icon, "si:")
|
icon = strings.TrimPrefix(icon, "si:")
|
||||||
icon = "https://cdnjs.cloudflare.com/ajax/libs/simple-icons/11.14.0/" + icon + ".svg"
|
icon = "https://cdn.jsdelivr.net/npm/simple-icons@latest/" + icon + ".svg"
|
||||||
|
|
||||||
return icon, true
|
return icon, true
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"html/template"
|
"html/template"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/glanceapp/glance/internal/assets"
|
"github.com/glanceapp/glance/internal/assets"
|
||||||
@ -12,7 +11,7 @@ import (
|
|||||||
|
|
||||||
type Group struct {
|
type Group struct {
|
||||||
widgetBase `yaml:",inline"`
|
widgetBase `yaml:",inline"`
|
||||||
Widgets Widgets `yaml:"widgets"`
|
containerWidgetBase `yaml:",inline"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (widget *Group) Initialize() error {
|
func (widget *Group) Initialize() error {
|
||||||
@ -23,7 +22,9 @@ func (widget *Group) Initialize() error {
|
|||||||
widget.Widgets[i].SetHideHeader(true)
|
widget.Widgets[i].SetHideHeader(true)
|
||||||
|
|
||||||
if widget.Widgets[i].GetType() == "group" {
|
if widget.Widgets[i].GetType() == "group" {
|
||||||
return errors.New("nested groups are not allowed")
|
return errors.New("nested groups are not supported")
|
||||||
|
} else if widget.Widgets[i].GetType() == "split-column" {
|
||||||
|
return errors.New("split columns inside of groups are not supported")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := widget.Widgets[i].Initialize(); err != nil {
|
if err := widget.Widgets[i].Initialize(); err != nil {
|
||||||
@ -35,40 +36,15 @@ func (widget *Group) Initialize() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (widget *Group) Update(ctx context.Context) {
|
func (widget *Group) Update(ctx context.Context) {
|
||||||
var wg sync.WaitGroup
|
widget.containerWidgetBase.Update(ctx)
|
||||||
now := time.Now()
|
|
||||||
|
|
||||||
for w := range widget.Widgets {
|
|
||||||
widget := widget.Widgets[w]
|
|
||||||
|
|
||||||
if !widget.RequiresUpdate(&now) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
widget.Update(ctx)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (widget *Group) SetProviders(providers *Providers) {
|
func (widget *Group) SetProviders(providers *Providers) {
|
||||||
for i := range widget.Widgets {
|
widget.containerWidgetBase.SetProviders(providers)
|
||||||
widget.Widgets[i].SetProviders(providers)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (widget *Group) RequiresUpdate(now *time.Time) bool {
|
func (widget *Group) RequiresUpdate(now *time.Time) bool {
|
||||||
for i := range widget.Widgets {
|
return widget.containerWidgetBase.RequiresUpdate(now)
|
||||||
if widget.Widgets[i].RequiresUpdate(now) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (widget *Group) Render() template.HTML {
|
func (widget *Group) Render() template.HTML {
|
||||||
|
@ -38,6 +38,10 @@ func (widget *Markets) Update(ctx context.Context) {
|
|||||||
markets.SortByAbsChange()
|
markets.SortByAbsChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if widget.Sort == "change" {
|
||||||
|
markets.SortByChange()
|
||||||
|
}
|
||||||
|
|
||||||
widget.Markets = markets
|
widget.Markets = markets
|
||||||
}
|
}
|
||||||
|
|
||||||
|
42
internal/widget/split-column.go
Normal file
42
internal/widget/split-column.go
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package widget
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"html/template"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/glanceapp/glance/internal/assets"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SplitColumn struct {
|
||||||
|
widgetBase `yaml:",inline"`
|
||||||
|
containerWidgetBase `yaml:",inline"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (widget *SplitColumn) Initialize() error {
|
||||||
|
widget.withError(nil).withTitle("Split Column").SetHideHeader(true)
|
||||||
|
|
||||||
|
for i := range widget.Widgets {
|
||||||
|
if err := widget.Widgets[i].Initialize(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (widget *SplitColumn) Update(ctx context.Context) {
|
||||||
|
widget.containerWidgetBase.Update(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (widget *SplitColumn) SetProviders(providers *Providers) {
|
||||||
|
widget.containerWidgetBase.SetProviders(providers)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (widget *SplitColumn) RequiresUpdate(now *time.Time) bool {
|
||||||
|
return widget.containerWidgetBase.RequiresUpdate(now)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (widget *SplitColumn) Render() template.HTML {
|
||||||
|
return widget.render(widget, assets.SplitColumnTemplate)
|
||||||
|
}
|
@ -67,6 +67,8 @@ func New(widgetType string) (Widget, error) {
|
|||||||
widget = &Group{}
|
widget = &Group{}
|
||||||
case "dns-stats":
|
case "dns-stats":
|
||||||
widget = &DNSStats{}
|
widget = &DNSStats{}
|
||||||
|
case "split-column":
|
||||||
|
widget = &SplitColumn{}
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unknown widget type: %s", widgetType)
|
return nil, fmt.Errorf("unknown widget type: %s", widgetType)
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user