Compare commits

...

16 Commits

Author SHA1 Message Date
Svilen Markov 24eff13ef2
Merge pull request #746 from ralphocdol/custom-api-object-range
Added Object helper for custom-api
2025-07-19 18:52:50 +07:00
Svilen Markov b4262eed65
Merge pull request #723 from khalt00/dev
Add allow login with enter button
2025-07-19 17:16:45 +07:00
Svilen Markov b8b1bf3c45
Merge pull request #734 from ralphocdol/removed-summary-tap-highlight
Fix DNS summary tap highlight
2025-07-19 17:11:47 +07:00
Svilen Markov 2065ab702d Hide highlight color globally 2025-07-19 17:10:33 +07:00
Svilen Markov fc4b766e92
Merge pull request #752 from igorkim/add-videos-sort-config
Add sort-by option for videos widget
2025-07-19 17:01:21 +07:00
Svilen Markov bd46dadd1b
Merge pull request #751 from GegudeBR/Add-Brave-Search
Add Brave as search engine
2025-07-19 16:59:12 +07:00
Svilen Markov 43e7496839
Merge pull request #767 from ralphocdol/theme-title-on-hover
Theme preset name on hover
2025-07-19 16:55:58 +07:00
Svilen Markov c1dd5b7ee0 Update docs and remove struct 2025-07-19 16:49:53 +07:00
Svilen Markov 38f1243b41 Update function for iterating over object entries 2025-07-19 16:43:40 +07:00
Ralph Ocdol 3c599b0c9e Add theme title on hover 2025-07-18 21:03:48 +07:00
Igor Kim d6de3b3437 Add sort config for videos widget 2025-06-28 02:53:37 +07:00
George 136f568d59 Add Brave as search engine 2025-06-26 17:28:28 +07:00
Ralph Ocdol d39e114efe added doc for Object helper 2025-06-23 20:38:43 +07:00
Ralph Ocdol aae0d71799 Added Object helper for custom-api 2025-06-23 20:18:05 +07:00
Ralph Ocdol fe44759293 Fix DNS summary tap highlight 2025-06-17 19:25:33 +07:00
Le Trong Kha 3b89602a01 Add allow login with enter button 2025-06-10 17:39:02 +07:00
8 changed files with 98 additions and 7 deletions

@ -870,6 +870,7 @@ Preview:
| playlists | array | no | |
| limit | integer | no | 25 |
| style | string | no | horizontal-cards |
| sort-by | string | no | posted |
| collapse-after | integer | no | 7 |
| collapse-after-rows | integer | no | 4 |
| include-shorts | boolean | no | false |
@ -905,6 +906,10 @@ https://www.youtube.com...&list={ID}&...
##### `limit`
The maximum number of videos to show.
##### `sort-by`
Used to specify the order in which the videos should get returned. Possible values are `none`, `updated`, and `posted`.
Default value is `posted`.
##### `collapse-after`
Specify the number of videos to show when using the `vertical-list` style before the "SHOW MORE" button appears.

@ -219,6 +219,38 @@ Output:
JSON response:
```json
{
"user": {
"id": 42,
"name": "Alice",
"active": true
}
}
```
To loop through each property of the object, you would use the following:
```html
{{ range $key, $value := .JSON.Entries "user" }}
<div>{{ $key }}: {{ $value.String "" }}</div>
{{ end }}
```
Output:
```html
<div>id: 42</div>
<div>name: Alice</div>
<div>active: true</div>
```
Each property in the object is exposed as a pair, with `$key` being a string and `$value` providing access to the value using the usual JSON methods.
<hr>
JSON response:
```json
{
"price": 100,
@ -414,6 +446,7 @@ The following functions are available on the `JSON` object:
- `Bool(key string) bool`: Returns the value of the key as a boolean.
- `Array(key string) []JSON`: Returns the value of the key as an array of `JSON` objects.
- `Exists(key string) bool`: Returns true if the key exists in the JSON object.
- `Entries(key string)`: Returns an iterator that allows you to loop through each property of the object. Example: `{{ range $key, $value := .JSON.Entries "user" }}`. This will yield pairs of key and value, where `$key` is a string and `$value` is a `JSON` object.
The following functions are available on the `Options` object:

@ -61,6 +61,10 @@ button {
width: 10px;
}
*:active {
-webkit-tap-highlight-color: transparent;
}
*:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 0.1rem;

@ -70,6 +70,18 @@ function enableLoginButtonIfCriteriaMet() {
);
}
function handleKeydown(event) {
if (event.key === "Enter") {
const isDisabled = loginButton.disabled;
if (!isDisabled) {
handleLoginAttempt();
}
}
}
usernameInput.on("keydown", handleKeydown);
passwordInput.on("keydown", handleKeydown);
usernameInput.on("input", enableLoginButtonIfCriteriaMet);
passwordInput.on("input", enableLoginButtonIfCriteriaMet);

@ -12,7 +12,7 @@
{{- end }}
{{- end }}
{{- if .NegativeColor }}{{ $negative = .NegativeColor.String | safeCSS }}{{ end }}
<button class="theme-preset{{ if .Light }} theme-preset-light{{ end }}" style="--color: {{ $background }}" data-key="{{ .Key }}">
<button class="theme-preset{{ if .Light }} theme-preset-light{{ end }}" style="--color: {{ $background }}" data-key="{{ .Key }}" title="{{ .Key }}">
<div class="theme-color" style="--color: {{ $primary }}"></div>
<div class="theme-color" style="--color: {{ $positive }}"></div>
<div class="theme-color" style="--color: {{ $negative }}"></div>

@ -8,6 +8,7 @@ import (
"fmt"
"html/template"
"io"
"iter"
"log/slog"
"math"
"net/http"
@ -415,6 +416,21 @@ func (r *decoratedGJSONResult) Get(key string) *decoratedGJSONResult {
return &decoratedGJSONResult{r.Result.Get(key)}
}
func (r *decoratedGJSONResult) Entries(key string) iter.Seq2[string, *decoratedGJSONResult] {
var obj gjson.Result
if key == "" {
obj = r.Result
} else {
obj = r.Result.Get(key)
}
return func(yield func(string, *decoratedGJSONResult) bool) {
obj.ForEach(func(k, v gjson.Result) bool {
return yield(k.String(), &decoratedGJSONResult{v})
})
}
}
func customAPIDoMathOp[T int | float64](a, b T, op string) T {
switch op {
case "add":

@ -35,9 +35,10 @@ var searchEngines = map[string]string{
"duckduckgo": "https://duckduckgo.com/?q={QUERY}",
"google": "https://www.google.com/search?q={QUERY}",
"bing": "https://www.bing.com/search?q={QUERY}",
"brave": "https://search.brave.com/search?q={QUERY}",
"perplexity": "https://www.perplexity.ai/search?q={QUERY}",
"kagi": "https://kagi.com/search?q={QUERY}",
"startpage": "https://www.startpage.com/search?q={QUERY}",
"kagi": "https://kagi.com/search?q={QUERY}",
"startpage": "https://www.startpage.com/search?q={QUERY}",
}
func (widget *searchWidget) initialize() error {

@ -31,6 +31,7 @@ type videosWidget struct {
Playlists []string `yaml:"playlists"`
Limit int `yaml:"limit"`
IncludeShorts bool `yaml:"include-shorts"`
SortBy string `yaml:"sort-by"`
}
func (widget *videosWidget) initialize() error {
@ -64,7 +65,7 @@ func (widget *videosWidget) initialize() error {
}
func (widget *videosWidget) update(ctx context.Context) {
videos, err := fetchYoutubeChannelUploads(widget.Channels, widget.VideoUrlTemplate, widget.IncludeShorts)
videos, err := fetchYoutubeChannelUploads(widget.Channels, widget.VideoUrlTemplate, widget.IncludeShorts, widget.SortBy)
if !widget.canContinueUpdateAfterHandlingErr(err) {
return
@ -98,6 +99,7 @@ type youtubeFeedResponseXml struct {
Videos []struct {
Title string `xml:"title"`
Published string `xml:"published"`
Updated string `xml:"updated"`
Link struct {
Href string `xml:"href,attr"`
} `xml:"link"`
@ -126,11 +128,12 @@ type video struct {
Author string
AuthorUrl string
TimePosted time.Time
TimeUpdated time.Time
}
type videoList []video
func (v videoList) sortByNewest() videoList {
func (v videoList) sortByPosted() videoList {
sort.Slice(v, func(i, j int) bool {
return v[i].TimePosted.After(v[j].TimePosted)
})
@ -138,7 +141,15 @@ func (v videoList) sortByNewest() videoList {
return v
}
func fetchYoutubeChannelUploads(channelOrPlaylistIDs []string, videoUrlTemplate string, includeShorts bool) (videoList, error) {
func (v videoList) sortByUpdated() videoList {
sort.Slice(v, func(i, j int) bool {
return v[i].TimeUpdated.After(v[j].TimeUpdated)
})
return v
}
func fetchYoutubeChannelUploads(channelOrPlaylistIDs []string, videoUrlTemplate string, includeShorts bool, sortBy string) (videoList, error) {
requests := make([]*http.Request, 0, len(channelOrPlaylistIDs))
for i := range channelOrPlaylistIDs {
@ -198,6 +209,7 @@ func fetchYoutubeChannelUploads(channelOrPlaylistIDs []string, videoUrlTemplate
Author: response.Channel,
AuthorUrl: response.ChannelLink + "/videos",
TimePosted: parseYoutubeFeedTime(v.Published),
TimeUpdated: parseYoutubeFeedTime(v.Updated),
})
}
}
@ -206,7 +218,15 @@ func fetchYoutubeChannelUploads(channelOrPlaylistIDs []string, videoUrlTemplate
return nil, errNoContent
}
videos.sortByNewest()
switch sortBy {
case "none":
case "updated":
videos.sortByUpdated()
case "posted":
videos.sortByPosted()
default: // "posted"
videos.sortByPosted()
}
if failed > 0 {
return videos, fmt.Errorf("%w: missing videos from %d channels", errPartialContent, failed)