From 3cfbe0c89b95971413df7f533c095db050f9ad9a Mon Sep 17 00:00:00 2001 From: David Leonard Date: Fri, 17 May 2024 14:37:05 -0700 Subject: [PATCH 001/105] Allow some branding customization. --- docs/configuration.md | 28 ++++++++++++++++++++++++++++ internal/assets/templates/page.html | 6 ++++-- internal/glance/config.go | 10 +++++++--- internal/glance/glance.go | 14 ++++++++++---- 4 files changed, 49 insertions(+), 9 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index e698fdd7..40ff0cca 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -3,6 +3,7 @@ - [Intro](#intro) - [Preconfigured page](#preconfigured-page) - [Server](#server) +- [Branding](#branding) - [Theme](#theme) - [Themes](#themes) - [Pages & Columns](#pages--columns) @@ -162,6 +163,33 @@ To be able to point to an asset from your assets path, use the `/assets/` path l icon: /assets/gitea-icon.png ``` +## Branding +You can adjust the various parts of the branding through a top level `branding` property. Example: + +```yaml +branding: + show: true + name: Glance + short-name: G +``` + +### Properties + +| Name | Type | Required | Default | +| ---- | ---- | -------- | ------- | +| show | bool | no | true | +| name | string | no | Glance | +| short-name | string | no | G | + +#### `show` +True will show the glance footer, false will hide it. + +#### `name` +Sets the name presented after the page name in the title. + +#### `short-name` +Sets the name presented before the pages in the header. + ## Theme Theming is done through a top level `theme` property. Values for the colors are in [HSL](https://giggster.com/guide/basics/hue-saturation-lightness/) (hue, saturation, lightness) format. You can use a color picker [like this one](https://hslpicker.com/) to convert colors from other formats to HSL. The values are separated by a space and `%` is not required for any of the numbers. diff --git a/internal/assets/templates/page.html b/internal/assets/templates/page.html index 61fad9e3..c50dfe25 100644 --- a/internal/assets/templates/page.html +++ b/internal/assets/templates/page.html @@ -1,6 +1,6 @@ {{ template "document.html" . }} -{{ define "document-title" }}{{ .Page.Title }} - Glance{{ end }} +{{ define "document-title" }}{{ .Page.Title }} - {{ .App.Config.Branding.Name }}{{ end }} {{ define "document-head-before" }} + + + {{ block "document-head-after" . }}{{ end }} diff --git a/internal/glance/glance.go b/internal/glance/glance.go index 653be75a..c1ce3e73 100644 --- a/internal/glance/glance.go +++ b/internal/glance/glance.go @@ -38,10 +38,10 @@ type Theme struct { } type Server struct { - Host string `yaml:"host"` - Port uint16 `yaml:"port"` - AssetsPath string `yaml:"assets-path"` - StartedAt time.Time `yaml:"-"` + Host string `yaml:"host"` + Port uint16 `yaml:"port"` + AssetsPath string `yaml:"assets-path"` + AssetsHash string `yaml:"-"` } 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 { + a.Config.Server.AssetsHash = assets.PublicFSHash + // TODO: add gzip support, static files must have their gzipped contents cached // TODO: add HTTPS support mux := http.NewServeMux() @@ -197,7 +203,10 @@ func (a *Application) Serve() error { mux.HandleFunc("GET /{$}", a.HandlePageRequest) mux.HandleFunc("GET /{page}", a.HandlePageRequest) 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 != "" { absAssetsPath, err := filepath.Abs(a.Config.Server.AssetsPath) @@ -216,8 +225,6 @@ func (a *Application) Serve() error { Handler: mux, } - a.Config.Server.StartedAt = time.Now() - slog.Info("Starting server", "host", a.Config.Server.Host, "port", a.Config.Server.Port) return server.ListenAndServe() } From 917d51e54bbbbaff803c1e5f4604fce7dac08242 Mon Sep 17 00:00:00 2001 From: Svilen Markov <7613769+svilenmarkov@users.noreply.github.com> Date: Tue, 11 Jun 2024 12:11:49 +0100 Subject: [PATCH 014/105] Make script a module --- internal/assets/static/main.js | 6 +----- internal/assets/templates/document.html | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/internal/assets/static/main.js b/internal/assets/static/main.js index fb86b02e..e8a200f8 100644 --- a/internal/assets/static/main.js +++ b/internal/assets/static/main.js @@ -570,8 +570,4 @@ async function setupPage() { } } -if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", setupPage); -} else { - setupPage(); -} +setupPage(); diff --git a/internal/assets/templates/document.html b/internal/assets/templates/document.html index e0447333..e43a4067 100644 --- a/internal/assets/templates/document.html +++ b/internal/assets/templates/document.html @@ -16,7 +16,7 @@ - + {{ block "document-head-after" . }}{{ end }} From b327e59ab11dde647d4c0d46538799a305c37ae1 Mon Sep 17 00:00:00 2001 From: Svilen Markov <7613769+svilenmarkov@users.noreply.github.com> Date: Sat, 29 Jun 2024 15:43:07 +0100 Subject: [PATCH 015/105] Allow specifying a custom lobsters URL or instance --- docs/configuration.md | 12 ++++++++++++ internal/feed/lobsters.go | 32 +++++++++++++++++++++----------- internal/widget/lobsters.go | 4 +++- 3 files changed, 36 insertions(+), 12 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 1e07bf2e..7946606a 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -573,11 +573,23 @@ Preview: #### Properties | Name | Type | Required | Default | | ---- | ---- | -------- | ------- | +| instance-url | string | no | https://lobste.rs/ | +| custom-url | string | no | | | limit | integer | no | 15 | | collapse-after | integer | no | 5 | | sort-by | string | no | hot | | 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` The maximum number of posts to show. diff --git a/internal/feed/lobsters.go b/internal/feed/lobsters.go index e103f56c..1bb54209 100644 --- a/internal/feed/lobsters.go +++ b/internal/feed/lobsters.go @@ -55,20 +55,30 @@ func getLobstersPostsFromFeed(feedUrl string) (ForumPosts, error) { 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 - if sortBy == "hot" { - sortBy = "hottest" - } else if sortBy == "new" { - sortBy = "newest" - } - - if len(tags) == 0 { - feedUrl = "https://lobste.rs/" + sortBy + ".json" + if customURL != "" { + feedUrl = customURL } else { - tags := strings.Join(tags, ",") - feedUrl = "https://lobste.rs/t/" + tags + ".json" + if instanceURL != "" { + 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) diff --git a/internal/widget/lobsters.go b/internal/widget/lobsters.go index d402db4e..61f82617 100644 --- a/internal/widget/lobsters.go +++ b/internal/widget/lobsters.go @@ -12,6 +12,8 @@ import ( type Lobsters struct { widgetBase `yaml:",inline"` Posts feed.ForumPosts `yaml:"-"` + InstanceURL string `yaml:"instance-url"` + CustomURL string `yaml:"custom-url"` Limit int `yaml:"limit"` CollapseAfter int `yaml:"collapse-after"` SortBy string `yaml:"sort-by"` @@ -38,7 +40,7 @@ func (widget *Lobsters) Initialize() error { } 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) { return From 4dbb5975c0aade035dbd6b3e852d7ba581fcda21 Mon Sep 17 00:00:00 2001 From: Svilen Markov <7613769+svilenmarkov@users.noreply.github.com> Date: Sat, 29 Jun 2024 15:55:17 +0100 Subject: [PATCH 016/105] Allow setting custom CSS classes for individual widgets --- docs/configuration.md | 6 ++++++ internal/assets/templates/widget-base.html | 4 ++-- internal/widget/widget.go | 1 + 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 7946606a..9a8c2be5 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -235,6 +235,8 @@ 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. ## Pages & Columns @@ -356,6 +358,7 @@ pages: | type | string | yes | | title | string | no | | cache | string | no | +| css-class | string | no | #### `type` Used to specify the widget. @@ -377,6 +380,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. +#### `css-class` +Set custom CSS classes for the specific widget instance. + ### RSS Display a list of articles from multiple RSS feeds. diff --git a/internal/assets/templates/widget-base.html b/internal/assets/templates/widget-base.html index e37d53f4..c32f9d00 100644 --- a/internal/assets/templates/widget-base.html +++ b/internal/assets/templates/widget-base.html @@ -1,4 +1,4 @@ -
+
{{ .Title }}
{{ if and .Error .ContentAvailable }} @@ -7,7 +7,7 @@
{{ end }}
-
+
{{ if .ContentAvailable }} {{ block "widget-content" . }}{{ end }} {{ else }} diff --git a/internal/widget/widget.go b/internal/widget/widget.go index 0ccb3de4..574db1df 100644 --- a/internal/widget/widget.go +++ b/internal/widget/widget.go @@ -119,6 +119,7 @@ const ( type widgetBase struct { Type string `yaml:"type"` Title string `yaml:"title"` + CSSClass string `yaml:"css-class"` CustomCacheDuration DurationField `yaml:"cache"` ContentAvailable bool `yaml:"-"` Error error `yaml:"-"` From 514cf2b81c69e20fb39565759ce0efa4836fae65 Mon Sep 17 00:00:00 2001 From: Svilen Markov <7613769+svilenmarkov@users.noreply.github.com> Date: Sat, 29 Jun 2024 16:10:43 +0100 Subject: [PATCH 017/105] Allow setting widget title URL --- docs/configuration.md | 4 ++++ internal/assets/templates/widget-base.html | 2 +- internal/widget/hacker-news.go | 5 ++++- internal/widget/lobsters.go | 6 ++++++ internal/widget/reddit.go | 5 ++++- internal/widget/twitch-channels.go | 5 ++++- internal/widget/twitch-top-games.go | 5 ++++- internal/widget/widget.go | 9 +++++++++ 8 files changed, 36 insertions(+), 5 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 9a8c2be5..2dffb2e7 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -357,6 +357,7 @@ pages: | ---- | ---- | -------- | | type | string | yes | | title | string | no | +| title-url | string | no | | cache | string | no | | css-class | string | no | @@ -366,6 +367,9 @@ Used to specify the widget. #### `title` 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` 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: diff --git a/internal/assets/templates/widget-base.html b/internal/assets/templates/widget-base.html index c32f9d00..eed89e1f 100644 --- a/internal/assets/templates/widget-base.html +++ b/internal/assets/templates/widget-base.html @@ -1,6 +1,6 @@
-
{{ .Title }}
+ {{ if ne "" .TitleURL}}{{ .Title }}{{ else }}
{{ .Title }}
{{ end }} {{ if and .Error .ContentAvailable }}
{{ else if .Notice }} diff --git a/internal/widget/hacker-news.go b/internal/widget/hacker-news.go index 9beccb78..f2db6e34 100644 --- a/internal/widget/hacker-news.go +++ b/internal/widget/hacker-news.go @@ -21,7 +21,10 @@ type HackerNews struct { } 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 { widget.Limit = 15 diff --git a/internal/widget/lobsters.go b/internal/widget/lobsters.go index 61f82617..a783c31e 100644 --- a/internal/widget/lobsters.go +++ b/internal/widget/lobsters.go @@ -24,6 +24,12 @@ type Lobsters struct { func (widget *Lobsters) Initialize() error { 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") { widget.SortBy = "hot" } diff --git a/internal/widget/reddit.go b/internal/widget/reddit.go index 0aa5f93c..1aa16fbe 100644 --- a/internal/widget/reddit.go +++ b/internal/widget/reddit.go @@ -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 } diff --git a/internal/widget/twitch-channels.go b/internal/widget/twitch-channels.go index 3b728b89..b06c986a 100644 --- a/internal/widget/twitch-channels.go +++ b/internal/widget/twitch-channels.go @@ -18,7 +18,10 @@ type TwitchChannels struct { } 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 { widget.CollapseAfter = 5 diff --git a/internal/widget/twitch-top-games.go b/internal/widget/twitch-top-games.go index 812c3c67..85933a64 100644 --- a/internal/widget/twitch-top-games.go +++ b/internal/widget/twitch-top-games.go @@ -18,7 +18,10 @@ type TwitchGames struct { } 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 { widget.Limit = 10 diff --git a/internal/widget/widget.go b/internal/widget/widget.go index 574db1df..324477f4 100644 --- a/internal/widget/widget.go +++ b/internal/widget/widget.go @@ -119,6 +119,7 @@ const ( type widgetBase struct { Type string `yaml:"type"` Title string `yaml:"title"` + TitleURL string `yaml:"title-url"` CSSClass string `yaml:"css-class"` CustomCacheDuration DurationField `yaml:"cache"` ContentAvailable bool `yaml:"-"` @@ -186,6 +187,14 @@ func (w *widgetBase) withTitle(title string) *widgetBase { 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 { w.cacheType = cacheTypeDuration From 39f40670d097ab32e518b02a80a1c0d48230f91a Mon Sep 17 00:00:00 2001 From: Svilen Markov <7613769+svilenmarkov@users.noreply.github.com> Date: Sat, 29 Jun 2024 17:17:27 +0100 Subject: [PATCH 018/105] Fix missing RSS thumbnail images --- internal/feed/rss.go | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/internal/feed/rss.go b/internal/feed/rss.go index 4d22af6e..74f064b6 100644 --- a/internal/feed/rss.go +++ b/internal/feed/rss.go @@ -12,6 +12,7 @@ import ( "time" "github.com/mmcdole/gofeed" + gofeedext "github.com/mmcdole/gofeed/extensions" ) type RSSFeedItem struct { @@ -145,6 +146,8 @@ func getItemsFromRSSFeedTask(request RSSFeedRequest) ([]RSSFeedItem, error) { if item.Image != nil { rssItem.ImageURL = item.Image.URL + } else if url := findThumbnailInItemExtensions(item); url != "" { + rssItem.ImageURL = url } else if feed.Image != nil { rssItem.ImageURL = feed.Image.URL } @@ -161,6 +164,36 @@ func getItemsFromRSSFeedTask(request RSSFeedRequest) ([]RSSFeedItem, error) { 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) { job := newJob(getItemsFromRSSFeedTask, requests).withWorkers(10) feeds, errs, err := workerPoolDo(job) From 3805f8b15647ac6950faab6b8ed1d94ebd83e2a9 Mon Sep 17 00:00:00 2001 From: dracarys18 Date: Wed, 3 Jul 2024 02:46:16 +0530 Subject: [PATCH 019/105] fix: allow custom bangs without an argument --- internal/assets/static/main.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/internal/assets/static/main.js b/internal/assets/static/main.js index 3e10b962..cc391e80 100644 --- a/internal/assets/static/main.js +++ b/internal/assets/static/main.js @@ -147,8 +147,7 @@ function setupSearchboxes() { query = input; searchUrlTemplate = defaultSearchUrl; } - - if (query.length == 0) { + if (query.length == 0 && currentBang == null) { return; } @@ -170,9 +169,13 @@ function setupSearchboxes() { } const handleInput = (event) => { - const value = event.target.value.trimStart(); - const words = value.split(" "); + const value = event.target.value.trim(); + if (value in bangsMap) { + changeCurrentBang(bangsMap[value]); + return; + } + const words = value.split(" "); if (words.length >= 2 && words[0] in bangsMap) { changeCurrentBang(bangsMap[words[0]]); return; From 7d1ede8c91f13b45ca287247d06b13fb4c21653f Mon Sep 17 00:00:00 2001 From: Albin Parou Date: Wed, 3 Jul 2024 08:17:51 +0200 Subject: [PATCH 020/105] releases: Add support for gitlab --- docs/configuration.md | 6 +- internal/feed/{github.go => git_forge.go} | 117 +++++++++++++++++++++- internal/widget/releases.go | 7 +- 3 files changed, 127 insertions(+), 3 deletions(-) rename internal/feed/{github.go => git_forge.go} (67%) diff --git a/docs/configuration.md b/docs/configuration.md index 2dffb2e7..393e4c4c 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1008,6 +1008,7 @@ Preview: | token | string | no | | | limit | integer | no | 10 | | collapse-after | integer | no | 5 | +| source | string | no | github | ##### `repositories` A list of repositores for which to fetch the latest release for. Only the name/repo is required, not the full URL. @@ -1039,7 +1040,10 @@ This way you can safely check your `glance.yml` in version control without expos The maximum number of releases to show. #### `collapse-after` -How many releases are visible before the "SHOW MORE" button appears. Set to `-1` to never collapse. +how many releases are visible before the "show more" button appears. set to `-1` to never collapse. + +#### `source` +Either `github` or `gitlab`. Wether to retrieve the releases from github repositories or gitlab repositories. ### Repository Display general information about a repository as well as a list of the latest open pull requests and issues. diff --git a/internal/feed/github.go b/internal/feed/git_forge.go similarity index 67% rename from internal/feed/github.go rename to internal/feed/git_forge.go index 4d7dc730..e46ecc56 100644 --- a/internal/feed/github.go +++ b/internal/feed/git_forge.go @@ -1,9 +1,11 @@ package feed import ( + "errors" "fmt" "log/slog" "net/http" + "net/url" "sync" "time" ) @@ -17,6 +19,19 @@ type githubReleaseLatestResponseJson struct { } `json:"reactions"` } +type gitlabReleaseResponseJson struct { + TagName string `json:"tag_name"` + PublishedAt string `json:"created_at"` + Links struct { + Self string `json:"self"` + } `json:"_links"` + Draft bool `json:"draft"` + PreRelease bool `json:"prerelease"` + Reactions struct { + Downvotes int `json:"-1"` + } `json:"reactions"` +} + func parseGithubTime(t string) time.Time { parsedTime, err := time.Parse("2006-01-02T15:04:05Z", t) @@ -27,7 +42,107 @@ func parseGithubTime(t string) time.Time { return parsedTime } -func FetchLatestReleasesFromGithub(repositories []string, token string) (AppReleases, error) { +func FetchLatestReleasesFromGitForge(repositories []string, token string, source string) (AppReleases, error) { + switch source { + case "github": + return fetchLatestReleasesFromGithub(repositories, token) + case "gitlab": + return fetchLatestReleasesFromGitlab(repositories, token) + default: + return nil, errors.New(fmt.Sprintf("Release source %s is invalid", source)) + } +} + +func fetchLatestReleasesFromGitlab(repositories []string, token string) (AppReleases, error) { + appReleases := make(AppReleases, 0, len(repositories)) + + if len(repositories) == 0 { + return appReleases, nil + } + + requests := make([]*http.Request, len(repositories)) + + for i, repository := range repositories { + request, _ := http.NewRequest("GET", fmt.Sprintf("https://gitlab.com/api/v4/projects/%s/releases/", url.QueryEscape(repository)), nil) + + if token != "" { + request.Header.Add("PRIVATE-TOKEN", token) + } + + requests[i] = request + } + + task := decodeJsonFromRequestTask[[]gitlabReleaseResponseJson](defaultClient) + job := newJob(task, requests).withWorkers(15) + responses, errs, err := workerPoolDo(job) + + if err != nil { + return nil, err + } + + var failed int + + for i := range responses { + if errs[i] != nil { + failed++ + slog.Error("Failed to fetch or parse gitlab release", "error", errs[i], "url", requests[i].URL) + continue + } + + releases := responses[i] + + if len(releases) < 1 { + failed++ + slog.Error("No releases found", "repository", repositories[i], "url", requests[i].URL) + continue + } + + var liveRelease *gitlabReleaseResponseJson + + for i := range releases { + release := &releases[i] + + if !release.Draft && !release.PreRelease { + liveRelease = release + break + } + } + + if liveRelease == nil { + slog.Error("No live release found", "repository", repositories[i], "url", requests[i].URL) + continue + } + + version := liveRelease.TagName + + if version[0] != 'v' { + version = "v" + version + } + + appReleases = append(appReleases, AppRelease{ + Name: repositories[i], + Version: version, + NotesUrl: liveRelease.Links.Self, + TimeReleased: parseGithubTime(liveRelease.PublishedAt), + Downvotes: liveRelease.Reactions.Downvotes, + }) + } + + if len(appReleases) == 0 { + return nil, ErrNoContent + } + + appReleases.SortByNewest() + + if failed > 0 { + return appReleases, fmt.Errorf("%w: could not get %d releases", ErrPartialContent, failed) + } + + return appReleases, nil +} + + +func fetchLatestReleasesFromGithub(repositories []string, token string) (AppReleases, error) { appReleases := make(AppReleases, 0, len(repositories)) if len(repositories) == 0 { diff --git a/internal/widget/releases.go b/internal/widget/releases.go index 77fe1031..cd28aaf7 100644 --- a/internal/widget/releases.go +++ b/internal/widget/releases.go @@ -16,6 +16,7 @@ type Releases struct { Token OptionalEnvString `yaml:"token"` Limit int `yaml:"limit"` CollapseAfter int `yaml:"collapse-after"` + Source string `yaml:"source"` } func (widget *Releases) Initialize() error { @@ -29,11 +30,15 @@ func (widget *Releases) Initialize() error { widget.CollapseAfter = 5 } + if widget.Source == "" { + widget.Source = "github" + } + return nil } func (widget *Releases) Update(ctx context.Context) { - releases, err := feed.FetchLatestReleasesFromGithub(widget.Repositories, string(widget.Token)) + releases, err := feed.FetchLatestReleasesFromGitForge(widget.Repositories, string(widget.Token), widget.Source) if !widget.canContinueUpdateAfterHandlingErr(err) { return From 16da25898fb087067a342f3dbeb43027637bf7b3 Mon Sep 17 00:00:00 2001 From: Svilen Markov <7613769+svilenmarkov@users.noreply.github.com> Date: Wed, 3 Jul 2024 19:48:58 +0100 Subject: [PATCH 021/105] Prevent users from modifying internal property --- internal/feed/rss.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/feed/rss.go b/internal/feed/rss.go index 3ca8b513..fb400b5d 100644 --- a/internal/feed/rss.go +++ b/internal/feed/rss.go @@ -61,7 +61,7 @@ type RSSFeedRequest struct { HideCategories bool `yaml:"hide-categories"` HideDescription bool `yaml:"hide-description"` ItemLinkPrefix string `yaml:"item-link-prefix"` - IsDetailed bool + IsDetailed bool `yaml:"-"` } type RSSFeedItems []RSSFeedItem From a07fbfd7b7613bba556abb9290e4649f775d976c Mon Sep 17 00:00:00 2001 From: Luca Crema Date: Wed, 10 Jul 2024 14:27:11 +0200 Subject: [PATCH 022/105] Added baseurl somewhere --- internal/assets/templates/document.html | 12 ++++++------ internal/glance/glance.go | 11 ++++++----- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/internal/assets/templates/document.html b/internal/assets/templates/document.html index d126d8b7..4d784bd8 100644 --- a/internal/assets/templates/document.html +++ b/internal/assets/templates/document.html @@ -11,12 +11,12 @@ - - - - - - + + + + + + {{ block "document-head-after" . }}{{ end }} diff --git a/internal/glance/glance.go b/internal/glance/glance.go index 653be75a..85200126 100644 --- a/internal/glance/glance.go +++ b/internal/glance/glance.go @@ -41,6 +41,7 @@ type Server struct { Host string `yaml:"host"` Port uint16 `yaml:"port"` AssetsPath string `yaml:"assets-path"` + BaseUrl string `yaml:"base-url"` StartedAt time.Time `yaml:"-"` } @@ -194,10 +195,10 @@ func (a *Application) Serve() error { // TODO: add HTTPS support mux := http.NewServeMux() - mux.HandleFunc("GET /{$}", a.HandlePageRequest) - mux.HandleFunc("GET /{page}", a.HandlePageRequest) - 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.HandleFunc(fmt.Sprintf("GET %s/{$}", a.Config.Server.BaseUrl), a.HandlePageRequest) + mux.HandleFunc(fmt.Sprintf("GET %s/{page}", a.Config.Server.BaseUrl), a.HandlePageRequest) + mux.HandleFunc(fmt.Sprintf("GET %s/api/pages/{page}/content/{$}", a.Config.Server.BaseUrl), a.HandlePageContentRequest) + mux.Handle(fmt.Sprintf("GET %s/static/{path...}", a.Config.Server.BaseUrl), http.StripPrefix(fmt.Sprintf("%s/static/", a.Config.Server.BaseUrl), FileServerWithCache(http.FS(assets.PublicFS), 2*time.Hour))) if a.Config.Server.AssetsPath != "" { absAssetsPath, err := filepath.Abs(a.Config.Server.AssetsPath) @@ -208,7 +209,7 @@ func (a *Application) Serve() error { slog.Info("Serving assets", "path", absAssetsPath) assetsFS := FileServerWithCache(http.Dir(a.Config.Server.AssetsPath), 2*time.Hour) - mux.Handle("/assets/{path...}", http.StripPrefix("/assets/", assetsFS)) + mux.Handle(fmt.Sprintf("%s/assets/{path...}", a.Config.Server.BaseUrl), http.StripPrefix("/assets/", assetsFS)) } server := http.Server{ From 23d6d411481e1eb10dc7f397691123706f48d4d9 Mon Sep 17 00:00:00 2001 From: CremaLuca Date: Wed, 10 Jul 2024 15:22:58 +0200 Subject: [PATCH 023/105] fix(serve): using paths without base url --- internal/assets/static/main.js | 6 +++--- internal/assets/templates/page.html | 5 +++-- internal/glance/glance.go | 14 +++++++------- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/internal/assets/static/main.js b/internal/assets/static/main.js index 9818ecf9..5426c296 100644 --- a/internal/assets/static/main.js +++ b/internal/assets/static/main.js @@ -21,10 +21,10 @@ function throttledDebounce(callback, maxDebounceTimes, debounceDelay) { }; -async function fetchPageContent(pageSlug) { +async function fetchPageContent(pageData) { // TODO: handle non 200 status codes/time outs // TODO: add retries - const response = await fetch(`/api/pages/${pageSlug}/content/`); + const response = await fetch(`${pageData.baseUrl}/api/pages/${pageData.slug}/content/`); const content = await response.text(); return content; @@ -336,7 +336,7 @@ function setupCollapsibleGrids() { async function setupPage() { const pageElement = document.getElementById("page"); const pageContentElement = document.getElementById("page-content"); - const pageContent = await fetchPageContent(pageData.slug); + const pageContent = await fetchPageContent(pageData); pageContentElement.innerHTML = pageContent; diff --git a/internal/assets/templates/page.html b/internal/assets/templates/page.html index 98bb111f..42e6bb26 100644 --- a/internal/assets/templates/page.html +++ b/internal/assets/templates/page.html @@ -6,6 +6,7 @@ {{ end }} @@ -14,13 +15,13 @@ {{ define "document-head-after" }} {{ template "page-style-overrides.gotmpl" . }} {{ if ne "" .App.Config.Theme.CustomCSSFile }} - + {{ end }} {{ end }} {{ define "navigation-links" }} {{ range .App.Config.Pages }} -{{ .Title }} +{{ .Title }} {{ end }} {{ end }} diff --git a/internal/glance/glance.go b/internal/glance/glance.go index 85200126..dd9e40b2 100644 --- a/internal/glance/glance.go +++ b/internal/glance/glance.go @@ -41,7 +41,7 @@ type Server struct { Host string `yaml:"host"` Port uint16 `yaml:"port"` AssetsPath string `yaml:"assets-path"` - BaseUrl string `yaml:"base-url"` + BaseUrl string `yaml:"base-url"` StartedAt time.Time `yaml:"-"` } @@ -195,10 +195,10 @@ func (a *Application) Serve() error { // TODO: add HTTPS support mux := http.NewServeMux() - mux.HandleFunc(fmt.Sprintf("GET %s/{$}", a.Config.Server.BaseUrl), a.HandlePageRequest) - mux.HandleFunc(fmt.Sprintf("GET %s/{page}", a.Config.Server.BaseUrl), a.HandlePageRequest) - mux.HandleFunc(fmt.Sprintf("GET %s/api/pages/{page}/content/{$}", a.Config.Server.BaseUrl), a.HandlePageContentRequest) - mux.Handle(fmt.Sprintf("GET %s/static/{path...}", a.Config.Server.BaseUrl), http.StripPrefix(fmt.Sprintf("%s/static/", a.Config.Server.BaseUrl), FileServerWithCache(http.FS(assets.PublicFS), 2*time.Hour))) + mux.HandleFunc("GET /{$}", a.HandlePageRequest) + mux.HandleFunc("GET /{page}", a.HandlePageRequest) + 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))) if a.Config.Server.AssetsPath != "" { absAssetsPath, err := filepath.Abs(a.Config.Server.AssetsPath) @@ -209,7 +209,7 @@ func (a *Application) Serve() error { slog.Info("Serving assets", "path", absAssetsPath) assetsFS := FileServerWithCache(http.Dir(a.Config.Server.AssetsPath), 2*time.Hour) - mux.Handle(fmt.Sprintf("%s/assets/{path...}", a.Config.Server.BaseUrl), http.StripPrefix("/assets/", assetsFS)) + mux.Handle("/assets/{path...}", http.StripPrefix("/assets/", assetsFS)) } server := http.Server{ @@ -219,6 +219,6 @@ func (a *Application) Serve() error { 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, "base url", a.Config.Server.BaseUrl) return server.ListenAndServe() } From 5a62a8afe95785350c1ced10c677c2d661161651 Mon Sep 17 00:00:00 2001 From: Svilen Markov <7613769+svilenmarkov@users.noreply.github.com> Date: Mon, 15 Jul 2024 16:30:12 +0100 Subject: [PATCH 024/105] Add autofocus property to search widget --- docs/configuration.md | 4 ++++ internal/assets/templates/search.html | 2 +- internal/widget/search.go | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index eaec5d03..d3cea7dd 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -755,6 +755,7 @@ Preview: | ---- | ---- | -------- | ------- | | search-engine | string | no | duckduckgo | | new-tab | boolean | no | false | +| autofocus | boolean | no | false | | bangs | array | no | | ##### `search-engine` @@ -768,6 +769,9 @@ 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` +When set to `true`, automatically focuses the search input on page load. + ##### `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: diff --git a/internal/assets/templates/search.html b/internal/assets/templates/search.html index 1b507cbf..df84e9da 100644 --- a/internal/assets/templates/search.html +++ b/internal/assets/templates/search.html @@ -16,7 +16,7 @@
- +
S diff --git a/internal/widget/search.go b/internal/widget/search.go index cf4c9d24..19ca372c 100644 --- a/internal/widget/search.go +++ b/internal/widget/search.go @@ -20,6 +20,7 @@ type Search struct { SearchEngine string `yaml:"search-engine"` Bangs []SearchBang `yaml:"bangs"` NewTab bool `yaml:"new-tab"` + Autofocus bool `yaml:"autofocus"` } func convertSearchUrl(url string) string { From 7cab07fa016e93e02ef910784509ef7e928fa4eb Mon Sep 17 00:00:00 2001 From: Svilen Markov <7613769+svilenmarkov@users.noreply.github.com> Date: Mon, 15 Jul 2024 17:01:26 +0100 Subject: [PATCH 025/105] Bump alpine version --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index a883a43d..a1c1ff43 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.20 +FROM alpine:3.20.1 ARG TARGETOS ARG TARGETARCH From e09ea6061601a054bae48157058aeb65a429c806 Mon Sep 17 00:00:00 2001 From: Svilen Markov <7613769+svilenmarkov@users.noreply.github.com> Date: Mon, 15 Jul 2024 17:02:20 +0100 Subject: [PATCH 026/105] Update dependencies --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index c2eb0064..3acda172 100644 --- a/go.mod +++ b/go.mod @@ -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.25.0 // indirect + golang.org/x/net v0.27.0 // indirect ) diff --git a/go.sum b/go.sum index 2ba89a64..28cb1ae0 100644 --- a/go.sum +++ b/go.sum @@ -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.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +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/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= From e0dee998d434427278fe82ec9dce829cadbba25a Mon Sep 17 00:00:00 2001 From: dvdandroid <6277172+DVDAndroid@users.noreply.github.com> Date: Tue, 23 Jul 2024 23:17:28 +0200 Subject: [PATCH 027/105] Make Twitch avatar clickable --- internal/assets/templates/twitch-channels.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/assets/templates/twitch-channels.html b/internal/assets/templates/twitch-channels.html index f053122e..8e23347e 100644 --- a/internal/assets/templates/twitch-channels.html +++ b/internal/assets/templates/twitch-channels.html @@ -7,7 +7,9 @@
{{ if .Exists }} - + + + {{ else }} From c9efdc2c16757999e4a0a486858906ad08e18666 Mon Sep 17 00:00:00 2001 From: dvdandroid <6277172+DVDAndroid@users.noreply.github.com> Date: Wed, 24 Jul 2024 21:38:35 +0200 Subject: [PATCH 028/105] Fix incorrect LiveSince if API returned no game (streamer can stream without a category/game associated) --- internal/feed/twitch.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/internal/feed/twitch.go b/internal/feed/twitch.go index 1ce93542..739d7d14 100644 --- a/internal/feed/twitch.go +++ b/internal/feed/twitch.go @@ -204,9 +204,11 @@ func fetchChannelFromTwitchTask(channel string) (TwitchChannel, error) { result.IsLive = true result.ViewersCount = channelShell.UserOrError.Stream.ViewersCount - if streamMetadata.UserOrNull != nil && streamMetadata.UserOrNull.Stream != nil && streamMetadata.UserOrNull.Stream.Game != nil { - result.Category = streamMetadata.UserOrNull.Stream.Game.Name - result.CategorySlug = streamMetadata.UserOrNull.Stream.Game.Slug + if streamMetadata.UserOrNull != nil && streamMetadata.UserOrNull.Stream != nil { + if streamMetadata.UserOrNull.Stream.Game != nil { + result.Category = streamMetadata.UserOrNull.Stream.Game.Name + result.CategorySlug = streamMetadata.UserOrNull.Stream.Game.Slug + } startedAt, err := time.Parse("2006-01-02T15:04:05Z", streamMetadata.UserOrNull.Stream.StartedAt) if err == nil { From 7bc9c704d14b9c844b830a74262b850db05b4640 Mon Sep 17 00:00:00 2001 From: dvdandroid <6277172+DVDAndroid@users.noreply.github.com> Date: Wed, 24 Jul 2024 21:50:00 +0200 Subject: [PATCH 029/105] Don't render "empty" game category --- internal/assets/templates/twitch-channels.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/assets/templates/twitch-channels.html b/internal/assets/templates/twitch-channels.html index f053122e..cce60e3b 100644 --- a/internal/assets/templates/twitch-channels.html +++ b/internal/assets/templates/twitch-channels.html @@ -18,7 +18,9 @@ {{ .Name }} {{ if .Exists }} {{ if .IsLive }} - {{ .Category }} + {{ if .Category }} + {{ .Category }} + {{ end }}