Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85b8ee493e | ||
|
|
e546488aeb | ||
|
|
f18620d890 | ||
|
|
24f93d8258 | ||
|
|
01016e08d2 | ||
|
|
949fde1517 | ||
|
|
804cf9916b | ||
|
|
ef24680baf | ||
|
|
448c795982 | ||
|
|
ee1d7599d5 | ||
|
|
d90d39933a | ||
|
|
7e38ab624a | ||
|
|
9a36187333 | ||
|
|
b0c4d70628 | ||
|
|
d5fa6424c1 | ||
|
|
07fe5b3cb1 | ||
|
|
672547cd07 | ||
|
|
28167403a4 | ||
|
|
eaf08d42dd | ||
|
|
bae95a5e07 |
@@ -138,6 +138,10 @@ A number between 1 and 65,535, so long as that port isn't already used by anythi
|
||||
#### `base-url`
|
||||
The base URL that Glance is hosted under. No need to specify this unless you're using a reverse proxy and are hosting Glance under a directory. If that's the case then you can set this value to `/glance` or whatever the directory is called. Note that the forward slash (`/`) in the beginning is required unless you specify the full domain and path.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> You need to strip the `base-url` prefix before forwarding the request to the Glance server.
|
||||
> In Caddy you can do this using [`handle_path`](https://caddyserver.com/docs/caddyfile/directives/handle_path) or [`uri strip_prefix`](https://caddyserver.com/docs/caddyfile/directives/uri).
|
||||
|
||||
#### `assets-path`
|
||||
The path to a directory that will be served by the server under the `/assets/` path. This is handy for widgets like the Monitor where you have to specify an icon URL and you want to self host all the icons rather than pointing to an external source.
|
||||
|
||||
@@ -277,6 +281,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.
|
||||
|
||||
@@ -848,7 +853,7 @@ Either a value from the table below or a URL to a custom search engine. Use `{QU
|
||||
##### `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`
|
||||
##### `autofocus`
|
||||
When set to `true`, automatically focuses the search input on page load.
|
||||
|
||||
##### `bangs`
|
||||
@@ -948,7 +953,7 @@ Display a widget provided by an external source (3rd party). If you want to lear
|
||||
| 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.**
|
||||
|
||||
##### `allow-potentially-dangerous-html`
|
||||
Whether to allow the extension to display HTML.
|
||||
|
||||
@@ -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
|
||||
|
||||

|
||||
|
||||
@@ -4,7 +4,7 @@ go 1.22.5
|
||||
|
||||
require (
|
||||
github.com/mmcdole/gofeed v1.3.0
|
||||
golang.org/x/text v0.16.0
|
||||
golang.org/x/text v0.21.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
@@ -15,5 +15,5 @@ require (
|
||||
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
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
)
|
||||
|
||||
@@ -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.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.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=
|
||||
@@ -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.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.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=
|
||||
|
||||
@@ -1,27 +1,5 @@
|
||||
import { setupPopovers } from './popover.js';
|
||||
|
||||
function throttledDebounce(callback, maxDebounceTimes, debounceDelay) {
|
||||
let debounceTimeout;
|
||||
let timesDebounced = 0;
|
||||
|
||||
return function () {
|
||||
if (timesDebounced == maxDebounceTimes) {
|
||||
clearTimeout(debounceTimeout);
|
||||
timesDebounced = 0;
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimeout(debounceTimeout);
|
||||
timesDebounced++;
|
||||
|
||||
debounceTimeout = setTimeout(() => {
|
||||
timesDebounced = 0;
|
||||
callback();
|
||||
}, debounceDelay);
|
||||
};
|
||||
};
|
||||
|
||||
import { throttledDebounce, isElementVisible } from './utils.js';
|
||||
|
||||
async function fetchPageContent(pageData) {
|
||||
// TODO: handle non 200 status codes/time outs
|
||||
@@ -427,7 +405,7 @@ function setupCollapsibleGrids() {
|
||||
|
||||
const button = attachExpandToggleButton(gridElement);
|
||||
|
||||
let cardsPerRow = 2;
|
||||
let cardsPerRow;
|
||||
|
||||
const resolveCollapsibleItems = () => {
|
||||
const hideItemsAfterIndex = cardsPerRow * collapseAfterRows;
|
||||
@@ -457,12 +435,11 @@ function setupCollapsibleGrids() {
|
||||
}
|
||||
};
|
||||
|
||||
afterContentReady(() => {
|
||||
cardsPerRow = getCardsPerRow();
|
||||
resolveCollapsibleItems();
|
||||
});
|
||||
const observer = new ResizeObserver(() => {
|
||||
if (!isElementVisible(gridElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.addEventListener("resize", () => {
|
||||
const newCardsPerRow = getCardsPerRow();
|
||||
|
||||
if (cardsPerRow == newCardsPerRow) {
|
||||
@@ -472,6 +449,8 @@ function setupCollapsibleGrids() {
|
||||
cardsPerRow = newCardsPerRow;
|
||||
resolveCollapsibleItems();
|
||||
});
|
||||
|
||||
afterContentReady(() => observer.observe(gridElement));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
export function throttledDebounce(callback, maxDebounceTimes, debounceDelay) {
|
||||
let debounceTimeout;
|
||||
let timesDebounced = 0;
|
||||
|
||||
return function () {
|
||||
if (timesDebounced == maxDebounceTimes) {
|
||||
clearTimeout(debounceTimeout);
|
||||
timesDebounced = 0;
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimeout(debounceTimeout);
|
||||
timesDebounced++;
|
||||
|
||||
debounceTimeout = setTimeout(() => {
|
||||
timesDebounced = 0;
|
||||
callback();
|
||||
}, debounceDelay);
|
||||
};
|
||||
};
|
||||
|
||||
export function isElementVisible(element) {
|
||||
return !!(element.offsetWidth || element.offsetHeight || element.getClientRects().length);
|
||||
}
|
||||
@@ -514,6 +514,7 @@ kbd:active {
|
||||
list-style: none;
|
||||
position: relative;
|
||||
display: flex;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.details[open] .summary {
|
||||
@@ -535,6 +536,10 @@ kbd:active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.details:not([open]) .list-with-transition {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.summary::after {
|
||||
content: "◀";
|
||||
font-size: 1.2em;
|
||||
@@ -707,6 +712,7 @@ details[open] .summary::after {
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
margin-bottom: 1.8rem;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.widget-error-header::before {
|
||||
@@ -1050,6 +1056,7 @@ details[open] .summary::after {
|
||||
border-radius: var(--border-radius);
|
||||
padding: 0.5rem;
|
||||
opacity: 0.7;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.bookmarks-icon {
|
||||
@@ -1094,7 +1101,6 @@ details[open] .summary::after {
|
||||
|
||||
.dns-stats-graph-gridlines-container {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
@@ -1121,7 +1127,6 @@ details[open] .summary::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 1px 0;
|
||||
z-index: -1;
|
||||
opacity: 0;
|
||||
background: var(--color-text-base);
|
||||
transition: opacity .2s;
|
||||
@@ -1263,7 +1268,6 @@ details[open] .summary::after {
|
||||
overflow: hidden;
|
||||
mask-image: linear-gradient(0deg, transparent 40%, #000);
|
||||
-webkit-mask-image: linear-gradient(0deg, transparent 40%, #000);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.weather-column-rain::before {
|
||||
|
||||
@@ -189,12 +189,19 @@ func FetchWeatherForPlace(place *PlaceJson, units string) (*Weather, error) {
|
||||
minT := slices.Min(temperatures)
|
||||
maxT := slices.Max(temperatures)
|
||||
|
||||
temperaturesRange := float64(maxT - minT)
|
||||
|
||||
for i := 0; i < 12; i++ {
|
||||
bars = append(bars, weatherColumn{
|
||||
Temperature: temperatures[i],
|
||||
Scale: float64(temperatures[i]-minT) / float64(maxT-minT),
|
||||
HasPrecipitation: precipitations[i],
|
||||
})
|
||||
|
||||
if temperaturesRange > 0 {
|
||||
bars[i].Scale = float64(temperatures[i]-minT) / temperaturesRange
|
||||
} else {
|
||||
bars[i].Scale = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+28
-7
@@ -1,6 +1,7 @@
|
||||
package feed
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
@@ -9,13 +10,33 @@ import (
|
||||
)
|
||||
|
||||
type piholeStatsResponse struct {
|
||||
TotalQueries int `json:"dns_queries_today"`
|
||||
QueriesSeries map[int64]int `json:"domains_over_time"`
|
||||
BlockedQueries int `json:"ads_blocked_today"`
|
||||
BlockedSeries map[int64]int `json:"ads_over_time"`
|
||||
BlockedPercentage float64 `json:"ads_percentage_today"`
|
||||
TopBlockedDomains map[string]int `json:"top_ads"`
|
||||
DomainsBlocked int `json:"domains_being_blocked"`
|
||||
TotalQueries int `json:"dns_queries_today"`
|
||||
QueriesSeries map[int64]int `json:"domains_over_time"`
|
||||
BlockedQueries int `json:"ads_blocked_today"`
|
||||
BlockedSeries map[int64]int `json:"ads_over_time"`
|
||||
BlockedPercentage float64 `json:"ads_percentage_today"`
|
||||
TopBlockedDomains piholeTopBlockedDomains `json:"top_ads"`
|
||||
DomainsBlocked int `json:"domains_being_blocked"`
|
||||
}
|
||||
|
||||
// If user has some level of privacy enabled on Pihole, `json:"top_ads"` is an empty array
|
||||
// Use custom unmarshal behavior to avoid not getting the rest of the valid data when unmarshalling
|
||||
type piholeTopBlockedDomains map[string]int
|
||||
|
||||
func (p *piholeTopBlockedDomains) UnmarshalJSON(data []byte) error {
|
||||
// NOTE: do not change to piholeTopBlockedDomains type here or it will cause a stack overflow
|
||||
// because of the UnmarshalJSON method getting called recursively
|
||||
temp := make(map[string]int)
|
||||
|
||||
err := json.Unmarshal(data, &temp)
|
||||
|
||||
if err != nil {
|
||||
*p = make(piholeTopBlockedDomains)
|
||||
} else {
|
||||
*p = temp
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func FetchPiholeStats(instanceURL, token string) (*DNSStats, error) {
|
||||
|
||||
@@ -111,8 +111,8 @@ func FetchSubredditPosts(subreddit, sort, topPeriod, search, commentsUrlTemplate
|
||||
TimePosted: time.Unix(int64(post.Time), 0),
|
||||
}
|
||||
|
||||
if post.Thumbnail != "" && post.Thumbnail != "self" && post.Thumbnail != "default" {
|
||||
forumPost.ThumbnailUrl = post.Thumbnail
|
||||
if post.Thumbnail != "" && post.Thumbnail != "self" && post.Thumbnail != "default" && post.Thumbnail != "nsfw" {
|
||||
forumPost.ThumbnailUrl = html.UnescapeString(post.Thumbnail)
|
||||
}
|
||||
|
||||
if !post.IsSelf {
|
||||
|
||||
@@ -275,6 +275,9 @@ func (a *Application) Serve() error {
|
||||
|
||||
mux.HandleFunc("GET /api/pages/{page}/content/{$}", a.HandlePageContentRequest)
|
||||
mux.HandleFunc("/api/widgets/{widget}/{path...}", a.HandleWidgetRequest)
|
||||
mux.HandleFunc("GET /api/healthz", func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
mux.Handle(
|
||||
fmt.Sprintf("GET /static/%s/{path...}", a.Config.Server.AssetsHash),
|
||||
|
||||
@@ -162,7 +162,7 @@ func toSimpleIconIfPrefixed(icon string) (string, bool) {
|
||||
}
|
||||
|
||||
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/icons/" + icon + ".svg"
|
||||
|
||||
return icon, true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user