Compare commits
26
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e09ea60616 | ||
|
|
7cab07fa01 | ||
|
|
5a62a8afe9 | ||
|
|
9bda88edbd | ||
|
|
16da25898f | ||
|
|
88ae2b8e83 | ||
|
|
3805f8b156 | ||
|
|
39f40670d0 | ||
|
|
514cf2b81c | ||
|
|
4dbb5975c0 | ||
|
|
b327e59ab1 | ||
|
|
917d51e54b | ||
|
|
122d05346b | ||
|
|
a2b7f59dc9 | ||
|
|
c23dc93c7a | ||
|
|
d3e3667405 | ||
|
|
d4ed5afe93 | ||
|
|
790ecef1c1 | ||
|
|
edb59ffdc6 | ||
|
|
e137038586 | ||
|
|
761eb343f9 | ||
|
|
6c15c907bf | ||
|
|
06b21e3d27 | ||
|
|
263d2e6f30 | ||
|
|
c97fec66f4 | ||
|
|
757f68c872 |
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
FROM alpine:3.20
|
FROM alpine:3.20.1
|
||||||
|
|
||||||
ARG TARGETOS
|
ARG TARGETOS
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
|
|||||||
+34
-1
@@ -40,7 +40,6 @@ pages:
|
|||||||
columns:
|
columns:
|
||||||
- size: small
|
- size: small
|
||||||
widgets:
|
widgets:
|
||||||
- type: clock
|
|
||||||
- type: calendar
|
- type: calendar
|
||||||
|
|
||||||
- type: rss
|
- type: rss
|
||||||
@@ -235,6 +234,8 @@ theme:
|
|||||||
> .widget-type-rss a {
|
> .widget-type-rss a {
|
||||||
> font-size: 1.5rem;
|
> 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.
|
||||||
|
|
||||||
|
|
||||||
## Pages & Columns
|
## Pages & Columns
|
||||||
@@ -355,7 +356,9 @@ pages:
|
|||||||
| ---- | ---- | -------- |
|
| ---- | ---- | -------- |
|
||||||
| type | string | yes |
|
| type | string | yes |
|
||||||
| title | string | no |
|
| title | string | no |
|
||||||
|
| title-url | string | no |
|
||||||
| cache | string | no |
|
| cache | string | no |
|
||||||
|
| css-class | string | no |
|
||||||
|
|
||||||
#### `type`
|
#### `type`
|
||||||
Used to specify the widget.
|
Used to specify the widget.
|
||||||
@@ -363,6 +366,9 @@ Used to specify the widget.
|
|||||||
#### `title`
|
#### `title`
|
||||||
The title of the widget. If left blank it will be defined by the widget.
|
The title of the widget. If left blank it will be defined by the widget.
|
||||||
|
|
||||||
|
#### `title-url`
|
||||||
|
The URL to go to when clicking on the widget's title. If left blank it will be defined by the widget (if available).
|
||||||
|
|
||||||
#### `cache`
|
#### `cache`
|
||||||
How long to keep the fetched data in memory. The value is a string and must be a number followed by one of s, m, h, d. Examples:
|
How long to keep the fetched data in memory. The value is a string and must be a number followed by one of s, m, h, d. Examples:
|
||||||
|
|
||||||
@@ -377,6 +383,9 @@ cache: 1d # 1 day
|
|||||||
>
|
>
|
||||||
> Not all widgets can have their cache duration modified. The calendar and weather widgets update on the hour and this cannot be changed.
|
> Not all widgets can have their cache duration modified. The calendar and weather widgets update on the hour and this cannot be changed.
|
||||||
|
|
||||||
|
#### `css-class`
|
||||||
|
Set custom CSS classes for the specific widget instance.
|
||||||
|
|
||||||
### RSS
|
### RSS
|
||||||
Display a list of articles from multiple RSS feeds.
|
Display a list of articles from multiple RSS feeds.
|
||||||
|
|
||||||
@@ -573,11 +582,23 @@ Preview:
|
|||||||
#### Properties
|
#### Properties
|
||||||
| Name | Type | Required | Default |
|
| Name | Type | Required | Default |
|
||||||
| ---- | ---- | -------- | ------- |
|
| ---- | ---- | -------- | ------- |
|
||||||
|
| instance-url | string | no | https://lobste.rs/ |
|
||||||
|
| custom-url | string | no | |
|
||||||
| limit | integer | no | 15 |
|
| limit | integer | no | 15 |
|
||||||
| collapse-after | integer | no | 5 |
|
| collapse-after | integer | no | 5 |
|
||||||
| sort-by | string | no | hot |
|
| sort-by | string | no | hot |
|
||||||
| tags | array | no | |
|
| tags | array | no | |
|
||||||
|
|
||||||
|
##### `instance-url`
|
||||||
|
The base URL for a lobsters instance hosted somewhere other than on lobste.rs. Example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
instance-url: https://www.journalduhacker.net/
|
||||||
|
```
|
||||||
|
|
||||||
|
##### `custom-url`
|
||||||
|
A custom URL to retrieve lobsters posts from. If this is specified, the `instance-url`, `sort-by` and `tags` properties are ignored.
|
||||||
|
|
||||||
##### `limit`
|
##### `limit`
|
||||||
The maximum number of posts to show.
|
The maximum number of posts to show.
|
||||||
|
|
||||||
@@ -725,10 +746,16 @@ Preview:
|
|||||||
| <kbd>Ctrl</kbd> + <kbd>Enter</kbd> | Perform search in a new tab | Search input is focused and not empty |
|
| <kbd>Ctrl</kbd> + <kbd>Enter</kbd> | Perform search in a new tab | Search input is focused and not empty |
|
||||||
| <kbd>Escape</kbd> | Leave focus | Search input is focused |
|
| <kbd>Escape</kbd> | Leave focus | Search input is focused |
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
>
|
||||||
|
> You can use the property `new-tab` with a value of `true` if you want to show search results in a new tab by default. <kbd>Ctrl</kbd> + <kbd>Enter</kbd> will then show results in the same tab.
|
||||||
|
|
||||||
#### Properties
|
#### Properties
|
||||||
| Name | Type | Required | Default |
|
| Name | Type | Required | Default |
|
||||||
| ---- | ---- | -------- | ------- |
|
| ---- | ---- | -------- | ------- |
|
||||||
| search-engine | string | no | duckduckgo |
|
| search-engine | string | no | duckduckgo |
|
||||||
|
| new-tab | boolean | no | false |
|
||||||
|
| autofocus | boolean | no | false |
|
||||||
| bangs | array | no | |
|
| bangs | array | no | |
|
||||||
|
|
||||||
##### `search-engine`
|
##### `search-engine`
|
||||||
@@ -739,6 +766,12 @@ Either a value from the table below or a URL to a custom search engine. Use `{QU
|
|||||||
| duckduckgo | `https://duckduckgo.com/?q={QUERY}` |
|
| duckduckgo | `https://duckduckgo.com/?q={QUERY}` |
|
||||||
| google | `https://www.google.com/search?q={QUERY}` |
|
| google | `https://www.google.com/search?q={QUERY}` |
|
||||||
|
|
||||||
|
##### `new-tab`
|
||||||
|
When set to `true`, swaps the shortcuts for showing results in the same or new tab, defaulting to showing results in a new tab.
|
||||||
|
|
||||||
|
##### `new-tab`
|
||||||
|
When set to `true`, automatically focuses the search input on page load.
|
||||||
|
|
||||||
##### `bangs`
|
##### `bangs`
|
||||||
What now? [Bangs](https://duckduckgo.com/bangs). They're shortcuts that allow you to use the same search box for many different sites. Assuming you have it configured, if for example you start your search input with `!yt` you'd be able to perform a search on YouTube:
|
What now? [Bangs](https://duckduckgo.com/bangs). They're shortcuts that allow you to use the same search box for many different sites. Assuming you have it configured, if for example you start your search input with `!yt` you'd be able to perform a search on YouTube:
|
||||||
|
|
||||||
|
|||||||
@@ -4,16 +4,16 @@ go 1.22.3
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/mmcdole/gofeed v1.3.0
|
github.com/mmcdole/gofeed v1.3.0
|
||||||
golang.org/x/text v0.14.0
|
golang.org/x/text v0.16.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/PuerkitoBio/goquery v1.9.1 // indirect
|
github.com/PuerkitoBio/goquery v1.9.2 // 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.24.0 // indirect
|
golang.org/x/net v0.27.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
github.com/PuerkitoBio/goquery v1.9.1 h1:mTL6XjbJTZdpfL+Gwl5U2h1l9yEkJjhmlTeV9VPW7UI=
|
github.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4yPeE=
|
||||||
github.com/PuerkitoBio/goquery v1.9.1/go.mod h1:cW1n6TmIMDoORQU5IU/P1T3tGFunOeXEpGP2WHRwkbY=
|
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 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=
|
||||||
@@ -33,8 +33,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
|
|||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
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.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.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
|
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||||
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||||
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=
|
||||||
@@ -54,8 +54,8 @@ 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.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.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.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||||
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,8 +1,14 @@
|
|||||||
package assets
|
package assets
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/md5"
|
||||||
"embed"
|
"embed"
|
||||||
|
"encoding/hex"
|
||||||
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
"log/slog"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed static
|
//go:embed static
|
||||||
@@ -13,3 +19,38 @@ var _templateFS embed.FS
|
|||||||
|
|
||||||
var PublicFS, _ = fs.Sub(_publicFS, "static")
|
var PublicFS, _ = fs.Sub(_publicFS, "static")
|
||||||
var TemplateFS, _ = fs.Sub(_templateFS, "templates")
|
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)
|
||||||
|
|||||||
@@ -263,6 +263,7 @@ html {
|
|||||||
a {
|
a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
|
overflow-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
@@ -731,6 +732,7 @@ kbd:active {
|
|||||||
height: 6rem;
|
height: 6rem;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
outline: none;
|
outline: none;
|
||||||
|
color: var(--color-text-highlight);
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-input::placeholder {
|
.search-input::placeholder {
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ function updateRelativeTimeForElements(elements)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupSearchboxes() {
|
function setupSearchBoxes() {
|
||||||
const searchWidgets = document.getElementsByClassName("search");
|
const searchWidgets = document.getElementsByClassName("search");
|
||||||
|
|
||||||
if (searchWidgets.length == 0) {
|
if (searchWidgets.length == 0) {
|
||||||
@@ -117,6 +117,7 @@ function setupSearchboxes() {
|
|||||||
for (let i = 0; i < searchWidgets.length; i++) {
|
for (let i = 0; i < searchWidgets.length; i++) {
|
||||||
const widget = searchWidgets[i];
|
const widget = searchWidgets[i];
|
||||||
const defaultSearchUrl = widget.dataset.defaultSearchUrl;
|
const defaultSearchUrl = widget.dataset.defaultSearchUrl;
|
||||||
|
const newTab = widget.dataset.newTab === "true";
|
||||||
const inputElement = widget.getElementsByClassName("search-input")[0];
|
const inputElement = widget.getElementsByClassName("search-input")[0];
|
||||||
const bangElement = widget.getElementsByClassName("search-bang")[0];
|
const bangElement = widget.getElementsByClassName("search-bang")[0];
|
||||||
const bangs = widget.querySelectorAll(".search-bangs > input");
|
const bangs = widget.querySelectorAll(".search-bangs > input");
|
||||||
@@ -147,14 +148,13 @@ function setupSearchboxes() {
|
|||||||
query = input;
|
query = input;
|
||||||
searchUrlTemplate = defaultSearchUrl;
|
searchUrlTemplate = defaultSearchUrl;
|
||||||
}
|
}
|
||||||
|
if (query.length == 0 && currentBang == null) {
|
||||||
if (query.length == 0) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = searchUrlTemplate.replace("!QUERY!", encodeURIComponent(query));
|
const url = searchUrlTemplate.replace("!QUERY!", encodeURIComponent(query));
|
||||||
|
|
||||||
if (event.ctrlKey) {
|
if (newTab && !event.ctrlKey || !newTab && event.ctrlKey) {
|
||||||
window.open(url, '_blank').focus();
|
window.open(url, '_blank').focus();
|
||||||
} else {
|
} else {
|
||||||
window.location.href = url;
|
window.location.href = url;
|
||||||
@@ -170,9 +170,13 @@ function setupSearchboxes() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleInput = (event) => {
|
const handleInput = (event) => {
|
||||||
const value = event.target.value.trimStart();
|
const value = event.target.value.trim();
|
||||||
const words = value.split(" ");
|
if (value in bangsMap) {
|
||||||
|
changeCurrentBang(bangsMap[value]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const words = value.split(" ");
|
||||||
if (words.length >= 2 && words[0] in bangsMap) {
|
if (words.length >= 2 && words[0] in bangsMap) {
|
||||||
changeCurrentBang(bangsMap[words[0]]);
|
changeCurrentBang(bangsMap[words[0]]);
|
||||||
return;
|
return;
|
||||||
@@ -551,7 +555,7 @@ async function setupPage() {
|
|||||||
try {
|
try {
|
||||||
setupClocks()
|
setupClocks()
|
||||||
setupCarousels();
|
setupCarousels();
|
||||||
setupSearchboxes();
|
setupSearchBoxes();
|
||||||
setupCollapsibleLists();
|
setupCollapsibleLists();
|
||||||
setupCollapsibleGrids();
|
setupCollapsibleGrids();
|
||||||
setupDynamicRelativeTime();
|
setupDynamicRelativeTime();
|
||||||
@@ -569,8 +573,4 @@ async function setupPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (document.readyState === "loading") {
|
setupPage();
|
||||||
document.addEventListener("DOMContentLoaded", setupPage);
|
|
||||||
} else {
|
|
||||||
setupPage();
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"start_url": "/",
|
"start_url": "/",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "/static/app-icon.png",
|
"src": "app-icon.png",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"sizes": "512x512"
|
"sizes": "512x512"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,12 +11,12 @@
|
|||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||||
<meta name="apple-mobile-web-app-title" content="Glance">
|
<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 }}">
|
<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="apple-touch-icon" sizes="512x512" href="{{ .App.AssetPath "app-icon.png" }}">
|
||||||
<link rel="icon" type="image/png" sizes="50x50" href="/static/favicon.png">
|
<link rel="icon" type="image/png" sizes="50x50" href="{{ .App.AssetPath "favicon.png" }}">
|
||||||
<link rel="manifest" href="/static/manifest.json">
|
<link rel="manifest" href="/static/manifest.json">
|
||||||
<link rel="icon" type="image/png" href="/static/favicon.png" />
|
<link rel="icon" type="image/png" href="{{ .App.AssetPath "favicon.png" }}" />
|
||||||
<link rel="stylesheet" href="/static/main.css?v={{ .App.Config.Server.StartedAt.Unix }}">
|
<link rel="stylesheet" href="{{ .App.AssetPath "main.css" }}">
|
||||||
<script async src="/static/main.js?v={{ .App.Config.Server.StartedAt.Unix }}"></script>
|
<script type="module" src="{{ .App.AssetPath "main.js" }}"></script>
|
||||||
{{ block "document-head-after" . }}{{ end }}
|
{{ block "document-head-after" . }}{{ end }}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
{{ define "widget-content-classes" }}widget-content-frameless{{ end }}
|
{{ define "widget-content-classes" }}widget-content-frameless{{ end }}
|
||||||
|
|
||||||
{{ define "widget-content" }}
|
{{ define "widget-content" }}
|
||||||
<div class="search widget-content-frame padding-inline-widget flex gap-15 items-center" data-default-search-url="{{ .SearchEngine }}">
|
<div class="search widget-content-frame padding-inline-widget flex gap-15 items-center" data-default-search-url="{{ .SearchEngine }}" data-new-tab="{{ .NewTab }}">
|
||||||
<div class="search-bangs">
|
<div class="search-bangs">
|
||||||
{{ range .Bangs }}
|
{{ range .Bangs }}
|
||||||
<input type="hidden" data-shortcut="{{ .Shortcut }}" data-title="{{ .Title }}" data-url="{{ .URL }}">
|
<input type="hidden" data-shortcut="{{ .Shortcut }}" data-title="{{ .Title }}" data-url="{{ .URL }}">
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input class="search-input" type="text" placeholder="Type here to search…" autocomplete="off">
|
<input class="search-input" type="text" placeholder="Type here to search…" autocomplete="off"{{ if .Autofocus }} autofocus{{ end }}>
|
||||||
|
|
||||||
<div class="search-bang"></div>
|
<div class="search-bang"></div>
|
||||||
<kbd class="hide-on-mobile" title="Press [S] to focus the search input">S</kbd>
|
<kbd class="hide-on-mobile" title="Press [S] to focus the search input">S</kbd>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<div class="widget widget-type-{{ .GetType }}">
|
<div class="widget widget-type-{{ .GetType }}{{ if ne "" .CSSClass }} {{ .CSSClass }}{{ end }}">
|
||||||
<div class="widget-header">
|
<div class="widget-header">
|
||||||
<div class="uppercase">{{ .Title }}</div>
|
{{ if ne "" .TitleURL}}<a href="{{ .TitleURL }}" target="_blank" rel="noreferrer" class="uppercase">{{ .Title }}</a>{{ else }}<div class="uppercase">{{ .Title }}</div>{{ end }}
|
||||||
{{ if and .Error .ContentAvailable }}
|
{{ if and .Error .ContentAvailable }}
|
||||||
<div class="notice-icon notice-icon-major" title="{{ .Error }}"></div>
|
<div class="notice-icon notice-icon-major" title="{{ .Error }}"></div>
|
||||||
{{ else if .Notice }}
|
{{ else if .Notice }}
|
||||||
<div class="notice-icon notice-icon-minor" title="{{ .Notice }}"></div>
|
<div class="notice-icon notice-icon-minor" title="{{ .Notice }}"></div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
<div class="widget-content {{ if .ContentAvailable }}{{ block "widget-content-classes" . }}{{ end }}{{ end }}">
|
<div class="widget-content{{ if .ContentAvailable }} {{ block "widget-content-classes" . }}{{ end }}{{ end }}">
|
||||||
{{ if .ContentAvailable }}
|
{{ if .ContentAvailable }}
|
||||||
{{ block "widget-content" . }}{{ end }}
|
{{ block "widget-content" . }}{{ end }}
|
||||||
{{ else }}
|
{{ else }}
|
||||||
|
|||||||
+4
-23
@@ -8,12 +8,10 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type githubReleaseResponseJson struct {
|
type githubReleaseLatestResponseJson struct {
|
||||||
TagName string `json:"tag_name"`
|
TagName string `json:"tag_name"`
|
||||||
PublishedAt string `json:"published_at"`
|
PublishedAt string `json:"published_at"`
|
||||||
HtmlUrl string `json:"html_url"`
|
HtmlUrl string `json:"html_url"`
|
||||||
Draft bool `json:"draft"`
|
|
||||||
PreRelease bool `json:"prerelease"`
|
|
||||||
Reactions struct {
|
Reactions struct {
|
||||||
Downvotes int `json:"-1"`
|
Downvotes int `json:"-1"`
|
||||||
} `json:"reactions"`
|
} `json:"reactions"`
|
||||||
@@ -39,7 +37,7 @@ func FetchLatestReleasesFromGithub(repositories []string, token string) (AppRele
|
|||||||
requests := make([]*http.Request, len(repositories))
|
requests := make([]*http.Request, len(repositories))
|
||||||
|
|
||||||
for i, repository := range repositories {
|
for i, repository := range repositories {
|
||||||
request, _ := http.NewRequest("GET", fmt.Sprintf("https://api.github.com/repos/%s/releases?per_page=10", repository), nil)
|
request, _ := http.NewRequest("GET", fmt.Sprintf("https://api.github.com/repos/%s/releases/latest", repository), nil)
|
||||||
|
|
||||||
if token != "" {
|
if token != "" {
|
||||||
request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
|
request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||||
@@ -48,7 +46,7 @@ func FetchLatestReleasesFromGithub(repositories []string, token string) (AppRele
|
|||||||
requests[i] = request
|
requests[i] = request
|
||||||
}
|
}
|
||||||
|
|
||||||
task := decodeJsonFromRequestTask[[]githubReleaseResponseJson](defaultClient)
|
task := decodeJsonFromRequestTask[githubReleaseLatestResponseJson](defaultClient)
|
||||||
job := newJob(task, requests).withWorkers(15)
|
job := newJob(task, requests).withWorkers(15)
|
||||||
responses, errs, err := workerPoolDo(job)
|
responses, errs, err := workerPoolDo(job)
|
||||||
|
|
||||||
@@ -65,24 +63,7 @@ func FetchLatestReleasesFromGithub(repositories []string, token string) (AppRele
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
releases := responses[i]
|
liveRelease := &responses[i]
|
||||||
|
|
||||||
if len(releases) < 1 {
|
|
||||||
failed++
|
|
||||||
slog.Error("No releases found", "repository", repositories[i], "url", requests[i].URL)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var liveRelease *githubReleaseResponseJson
|
|
||||||
|
|
||||||
for i := range releases {
|
|
||||||
release := &releases[i]
|
|
||||||
|
|
||||||
if !release.Draft && !release.PreRelease {
|
|
||||||
liveRelease = release
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if liveRelease == nil {
|
if liveRelease == nil {
|
||||||
slog.Error("No live release found", "repository", repositories[i], "url", requests[i].URL)
|
slog.Error("No live release found", "repository", repositories[i], "url", requests[i].URL)
|
||||||
|
|||||||
+21
-11
@@ -55,20 +55,30 @@ func getLobstersPostsFromFeed(feedUrl string) (ForumPosts, error) {
|
|||||||
return posts, nil
|
return posts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func FetchLobstersPosts(sortBy string, tags []string) (ForumPosts, error) {
|
func FetchLobstersPosts(customURL string, instanceURL string, sortBy string, tags []string) (ForumPosts, error) {
|
||||||
var feedUrl string
|
var feedUrl string
|
||||||
|
|
||||||
if sortBy == "hot" {
|
if customURL != "" {
|
||||||
sortBy = "hottest"
|
feedUrl = customURL
|
||||||
} else if sortBy == "new" {
|
|
||||||
sortBy = "newest"
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(tags) == 0 {
|
|
||||||
feedUrl = "https://lobste.rs/" + sortBy + ".json"
|
|
||||||
} else {
|
} else {
|
||||||
tags := strings.Join(tags, ",")
|
if instanceURL != "" {
|
||||||
feedUrl = "https://lobste.rs/t/" + tags + ".json"
|
instanceURL = strings.TrimRight(instanceURL, "/") + "/"
|
||||||
|
} else {
|
||||||
|
instanceURL = "https://lobste.rs/"
|
||||||
|
}
|
||||||
|
|
||||||
|
if sortBy == "hot" {
|
||||||
|
sortBy = "hottest"
|
||||||
|
} else if sortBy == "new" {
|
||||||
|
sortBy = "newest"
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tags) == 0 {
|
||||||
|
feedUrl = instanceURL + sortBy + ".json"
|
||||||
|
} else {
|
||||||
|
tags := strings.Join(tags, ",")
|
||||||
|
feedUrl = instanceURL + "t/" + tags + ".json"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
posts, err := getLobstersPostsFromFeed(feedUrl)
|
posts, err := getLobstersPostsFromFeed(feedUrl)
|
||||||
|
|||||||
+70
-25
@@ -12,6 +12,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mmcdole/gofeed"
|
"github.com/mmcdole/gofeed"
|
||||||
|
gofeedext "github.com/mmcdole/gofeed/extensions"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RSSFeedItem struct {
|
type RSSFeedItem struct {
|
||||||
@@ -43,12 +44,25 @@ func sanitizeFeedDescription(description string) string {
|
|||||||
return description
|
return description
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func shortenFeedDescriptionLen(description string, maxLen int) string {
|
||||||
|
description, _ = limitStringLength(description, 1000)
|
||||||
|
description = sanitizeFeedDescription(description)
|
||||||
|
description, limited := limitStringLength(description, maxLen)
|
||||||
|
|
||||||
|
if limited {
|
||||||
|
description += "…"
|
||||||
|
}
|
||||||
|
|
||||||
|
return description
|
||||||
|
}
|
||||||
|
|
||||||
type RSSFeedRequest struct {
|
type RSSFeedRequest struct {
|
||||||
Url string `yaml:"url"`
|
Url string `yaml:"url"`
|
||||||
Title string `yaml:"title"`
|
Title string `yaml:"title"`
|
||||||
HideCategories bool `yaml:"hide-categories"`
|
HideCategories bool `yaml:"hide-categories"`
|
||||||
HideDescription bool `yaml:"hide-description"`
|
HideDescription bool `yaml:"hide-description"`
|
||||||
ItemLinkPrefix string `yaml:"item-link-prefix"`
|
ItemLinkPrefix string `yaml:"item-link-prefix"`
|
||||||
|
IsDetailed bool `yaml:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RSSFeedItems []RSSFeedItem
|
type RSSFeedItems []RSSFeedItem
|
||||||
@@ -80,7 +94,6 @@ func getItemsFromRSSFeedTask(request RSSFeedRequest) ([]RSSFeedItem, error) {
|
|||||||
|
|
||||||
rssItem := RSSFeedItem{
|
rssItem := RSSFeedItem{
|
||||||
ChannelURL: feed.Link,
|
ChannelURL: feed.Link,
|
||||||
Title: item.Title,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if request.ItemLinkPrefix != "" {
|
if request.ItemLinkPrefix != "" {
|
||||||
@@ -107,34 +120,34 @@ func getItemsFromRSSFeedTask(request RSSFeedRequest) ([]RSSFeedItem, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !request.HideDescription && item.Description != "" {
|
if item.Title != "" {
|
||||||
description, _ := limitStringLength(item.Description, 1000)
|
rssItem.Title = item.Title
|
||||||
description = sanitizeFeedDescription(description)
|
} else {
|
||||||
description, limited := limitStringLength(description, 200)
|
rssItem.Title = shortenFeedDescriptionLen(item.Description, 100)
|
||||||
|
|
||||||
if limited {
|
|
||||||
description += "…"
|
|
||||||
}
|
|
||||||
|
|
||||||
rssItem.Description = description
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !request.HideCategories {
|
if request.IsDetailed {
|
||||||
var categories = make([]string, 0, 6)
|
if !request.HideDescription && item.Description != "" && item.Title != "" {
|
||||||
|
rssItem.Description = shortenFeedDescriptionLen(item.Description, 200)
|
||||||
for _, category := range item.Categories {
|
|
||||||
if len(categories) == 6 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(category) == 0 || len(category) > 30 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
categories = append(categories, category)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rssItem.Categories = categories
|
if !request.HideCategories {
|
||||||
|
var categories = make([]string, 0, 6)
|
||||||
|
|
||||||
|
for _, category := range item.Categories {
|
||||||
|
if len(categories) == 6 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(category) == 0 || len(category) > 30 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
categories = append(categories, category)
|
||||||
|
}
|
||||||
|
|
||||||
|
rssItem.Categories = categories
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if request.Title != "" {
|
if request.Title != "" {
|
||||||
@@ -145,6 +158,8 @@ func getItemsFromRSSFeedTask(request RSSFeedRequest) ([]RSSFeedItem, error) {
|
|||||||
|
|
||||||
if item.Image != nil {
|
if item.Image != nil {
|
||||||
rssItem.ImageURL = item.Image.URL
|
rssItem.ImageURL = item.Image.URL
|
||||||
|
} else if url := findThumbnailInItemExtensions(item); url != "" {
|
||||||
|
rssItem.ImageURL = url
|
||||||
} else if feed.Image != nil {
|
} else if feed.Image != nil {
|
||||||
rssItem.ImageURL = feed.Image.URL
|
rssItem.ImageURL = feed.Image.URL
|
||||||
}
|
}
|
||||||
@@ -161,6 +176,36 @@ func getItemsFromRSSFeedTask(request RSSFeedRequest) ([]RSSFeedItem, error) {
|
|||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func recursiveFindThumbnailInExtensions(extensions map[string][]gofeedext.Extension) string {
|
||||||
|
for _, exts := range extensions {
|
||||||
|
for _, ext := range exts {
|
||||||
|
if ext.Name == "thumbnail" || ext.Name == "image" {
|
||||||
|
if url, ok := ext.Attrs["url"]; ok {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ext.Children != nil {
|
||||||
|
if url := recursiveFindThumbnailInExtensions(ext.Children); url != "" {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func findThumbnailInItemExtensions(item *gofeed.Item) string {
|
||||||
|
media, ok := item.Extensions["media"]
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return recursiveFindThumbnailInExtensions(media)
|
||||||
|
}
|
||||||
|
|
||||||
func GetItemsFromRSSFeeds(requests []RSSFeedRequest) (RSSFeedItems, error) {
|
func GetItemsFromRSSFeeds(requests []RSSFeedRequest) (RSSFeedItems, error) {
|
||||||
job := newJob(getItemsFromRSSFeedTask, requests).withWorkers(10)
|
job := newJob(getItemsFromRSSFeedTask, requests).withWorkers(10)
|
||||||
feeds, errs, err := workerPoolDo(job)
|
feeds, errs, err := workerPoolDo(job)
|
||||||
|
|||||||
@@ -38,10 +38,10 @@ type Theme struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
Host string `yaml:"host"`
|
Host string `yaml:"host"`
|
||||||
Port uint16 `yaml:"port"`
|
Port uint16 `yaml:"port"`
|
||||||
AssetsPath string `yaml:"assets-path"`
|
AssetsPath string `yaml:"assets-path"`
|
||||||
StartedAt time.Time `yaml:"-"`
|
AssetsHash string `yaml:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Column struct {
|
type Column struct {
|
||||||
@@ -189,7 +189,13 @@ func FileServerWithCache(fs http.FileSystem, cacheDuration time.Duration) http.H
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *Application) AssetPath(asset string) string {
|
||||||
|
return "/static/" + a.Config.Server.AssetsHash + "/" + asset
|
||||||
|
}
|
||||||
|
|
||||||
func (a *Application) Serve() error {
|
func (a *Application) Serve() error {
|
||||||
|
a.Config.Server.AssetsHash = assets.PublicFSHash
|
||||||
|
|
||||||
// TODO: add gzip support, static files must have their gzipped contents cached
|
// TODO: add gzip support, static files must have their gzipped contents cached
|
||||||
// TODO: add HTTPS support
|
// TODO: add HTTPS support
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
@@ -197,7 +203,10 @@ func (a *Application) Serve() error {
|
|||||||
mux.HandleFunc("GET /{$}", a.HandlePageRequest)
|
mux.HandleFunc("GET /{$}", a.HandlePageRequest)
|
||||||
mux.HandleFunc("GET /{page}", a.HandlePageRequest)
|
mux.HandleFunc("GET /{page}", a.HandlePageRequest)
|
||||||
mux.HandleFunc("GET /api/pages/{page}/content/{$}", a.HandlePageContentRequest)
|
mux.HandleFunc("GET /api/pages/{page}/content/{$}", a.HandlePageContentRequest)
|
||||||
mux.Handle("GET /static/{path...}", http.StripPrefix("/static/", FileServerWithCache(http.FS(assets.PublicFS), 2*time.Hour)))
|
mux.Handle(
|
||||||
|
fmt.Sprintf("GET /static/%s/{path...}", a.Config.Server.AssetsHash),
|
||||||
|
http.StripPrefix("/static/"+a.Config.Server.AssetsHash, FileServerWithCache(http.FS(assets.PublicFS), 8*time.Hour)),
|
||||||
|
)
|
||||||
|
|
||||||
if a.Config.Server.AssetsPath != "" {
|
if a.Config.Server.AssetsPath != "" {
|
||||||
absAssetsPath, err := filepath.Abs(a.Config.Server.AssetsPath)
|
absAssetsPath, err := filepath.Abs(a.Config.Server.AssetsPath)
|
||||||
@@ -216,8 +225,6 @@ func (a *Application) Serve() error {
|
|||||||
Handler: mux,
|
Handler: mux,
|
||||||
}
|
}
|
||||||
|
|
||||||
a.Config.Server.StartedAt = time.Now()
|
|
||||||
|
|
||||||
slog.Info("Starting server", "host", a.Config.Server.Host, "port", a.Config.Server.Port)
|
slog.Info("Starting server", "host", a.Config.Server.Host, "port", a.Config.Server.Port)
|
||||||
return server.ListenAndServe()
|
return server.ListenAndServe()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,10 @@ type HackerNews struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (widget *HackerNews) Initialize() error {
|
func (widget *HackerNews) Initialize() error {
|
||||||
widget.withTitle("Hacker News").withCacheDuration(30 * time.Minute)
|
widget.
|
||||||
|
withTitle("Hacker News").
|
||||||
|
withTitleURL("https://news.ycombinator.com/").
|
||||||
|
withCacheDuration(30 * time.Minute)
|
||||||
|
|
||||||
if widget.Limit <= 0 {
|
if widget.Limit <= 0 {
|
||||||
widget.Limit = 15
|
widget.Limit = 15
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import (
|
|||||||
type Lobsters struct {
|
type Lobsters struct {
|
||||||
widgetBase `yaml:",inline"`
|
widgetBase `yaml:",inline"`
|
||||||
Posts feed.ForumPosts `yaml:"-"`
|
Posts feed.ForumPosts `yaml:"-"`
|
||||||
|
InstanceURL string `yaml:"instance-url"`
|
||||||
|
CustomURL string `yaml:"custom-url"`
|
||||||
Limit int `yaml:"limit"`
|
Limit int `yaml:"limit"`
|
||||||
CollapseAfter int `yaml:"collapse-after"`
|
CollapseAfter int `yaml:"collapse-after"`
|
||||||
SortBy string `yaml:"sort-by"`
|
SortBy string `yaml:"sort-by"`
|
||||||
@@ -20,7 +22,13 @@ type Lobsters struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (widget *Lobsters) Initialize() error {
|
func (widget *Lobsters) Initialize() error {
|
||||||
widget.withTitle("Lobsters").withCacheDuration(30 * time.Minute)
|
widget.withTitle("Lobsters").withCacheDuration(time.Hour)
|
||||||
|
|
||||||
|
if widget.InstanceURL == "" {
|
||||||
|
widget.withTitleURL("https://lobste.rs")
|
||||||
|
} else {
|
||||||
|
widget.withTitleURL(widget.InstanceURL)
|
||||||
|
}
|
||||||
|
|
||||||
if widget.SortBy == "" || (widget.SortBy != "hot" && widget.SortBy != "new") {
|
if widget.SortBy == "" || (widget.SortBy != "hot" && widget.SortBy != "new") {
|
||||||
widget.SortBy = "hot"
|
widget.SortBy = "hot"
|
||||||
@@ -38,7 +46,7 @@ func (widget *Lobsters) Initialize() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (widget *Lobsters) Update(ctx context.Context) {
|
func (widget *Lobsters) Update(ctx context.Context) {
|
||||||
posts, err := feed.FetchLobstersPosts(widget.SortBy, widget.Tags)
|
posts, err := feed.FetchLobstersPosts(widget.CustomURL, widget.InstanceURL, widget.SortBy, widget.Tags)
|
||||||
|
|
||||||
if !widget.canContinueUpdateAfterHandlingErr(err) {
|
if !widget.canContinueUpdateAfterHandlingErr(err) {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -54,7 +54,10 @@ func (widget *Reddit) Initialize() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
widget.withTitle("/r/" + widget.Subreddit).withCacheDuration(30 * time.Minute)
|
widget.
|
||||||
|
withTitle("/r/" + widget.Subreddit).
|
||||||
|
withTitleURL("https://www.reddit.com/r/" + widget.Subreddit + "/").
|
||||||
|
withCacheDuration(30 * time.Minute)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,10 +39,9 @@ func (widget *RSS) Initialize() error {
|
|||||||
widget.CardHeight = 0
|
widget.CardHeight = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
if widget.Style != "detailed-list" {
|
if widget.Style == "detailed-list" {
|
||||||
for i := range widget.FeedRequests {
|
for i := range widget.FeedRequests {
|
||||||
widget.FeedRequests[i].HideCategories = true
|
widget.FeedRequests[i].IsDetailed = true
|
||||||
widget.FeedRequests[i].HideDescription = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ type Search struct {
|
|||||||
cachedHTML template.HTML `yaml:"-"`
|
cachedHTML template.HTML `yaml:"-"`
|
||||||
SearchEngine string `yaml:"search-engine"`
|
SearchEngine string `yaml:"search-engine"`
|
||||||
Bangs []SearchBang `yaml:"bangs"`
|
Bangs []SearchBang `yaml:"bangs"`
|
||||||
|
NewTab bool `yaml:"new-tab"`
|
||||||
|
Autofocus bool `yaml:"autofocus"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertSearchUrl(url string) string {
|
func convertSearchUrl(url string) string {
|
||||||
|
|||||||
@@ -18,7 +18,10 @@ type TwitchChannels struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (widget *TwitchChannels) Initialize() error {
|
func (widget *TwitchChannels) Initialize() error {
|
||||||
widget.withTitle("Twitch Channels").withCacheDuration(time.Minute * 10)
|
widget.
|
||||||
|
withTitle("Twitch Channels").
|
||||||
|
withTitleURL("https://www.twitch.tv/directory/following").
|
||||||
|
withCacheDuration(time.Minute * 10)
|
||||||
|
|
||||||
if widget.CollapseAfter == 0 || widget.CollapseAfter < -1 {
|
if widget.CollapseAfter == 0 || widget.CollapseAfter < -1 {
|
||||||
widget.CollapseAfter = 5
|
widget.CollapseAfter = 5
|
||||||
|
|||||||
@@ -18,7 +18,10 @@ type TwitchGames struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (widget *TwitchGames) Initialize() error {
|
func (widget *TwitchGames) Initialize() error {
|
||||||
widget.withTitle("Top games on Twitch").withCacheDuration(time.Minute * 10)
|
widget.
|
||||||
|
withTitle("Top games on Twitch").
|
||||||
|
withTitleURL("https://www.twitch.tv/directory?sort=VIEWER_COUNT").
|
||||||
|
withCacheDuration(time.Minute * 10)
|
||||||
|
|
||||||
if widget.Limit <= 0 {
|
if widget.Limit <= 0 {
|
||||||
widget.Limit = 10
|
widget.Limit = 10
|
||||||
|
|||||||
@@ -119,6 +119,8 @@ const (
|
|||||||
type widgetBase struct {
|
type widgetBase struct {
|
||||||
Type string `yaml:"type"`
|
Type string `yaml:"type"`
|
||||||
Title string `yaml:"title"`
|
Title string `yaml:"title"`
|
||||||
|
TitleURL string `yaml:"title-url"`
|
||||||
|
CSSClass string `yaml:"css-class"`
|
||||||
CustomCacheDuration DurationField `yaml:"cache"`
|
CustomCacheDuration DurationField `yaml:"cache"`
|
||||||
ContentAvailable bool `yaml:"-"`
|
ContentAvailable bool `yaml:"-"`
|
||||||
Error error `yaml:"-"`
|
Error error `yaml:"-"`
|
||||||
@@ -185,6 +187,14 @@ func (w *widgetBase) withTitle(title string) *widgetBase {
|
|||||||
return w
|
return w
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *widgetBase) withTitleURL(titleURL string) *widgetBase {
|
||||||
|
if w.TitleURL == "" {
|
||||||
|
w.TitleURL = titleURL
|
||||||
|
}
|
||||||
|
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
func (w *widgetBase) withCacheDuration(duration time.Duration) *widgetBase {
|
func (w *widgetBase) withCacheDuration(duration time.Duration) *widgetBase {
|
||||||
w.cacheType = cacheTypeDuration
|
w.cacheType = cacheTypeDuration
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user