Skip to content

feat: add cards style layout for bookmarks widget #611

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: dev
Choose a base branch
from
16 changes: 13 additions & 3 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -997,6 +997,7 @@ Preview:
| search-engine | string | no | duckduckgo |
| new-tab | boolean | no | false |
| autofocus | boolean | no | false |
| target | string | no | _blank |
| placeholder | string | no | Type here to search… |
| bangs | array | no | |

Expand All @@ -1018,6 +1019,9 @@ When set to `true`, swaps the shortcuts for showing results in the same or new t
##### `autofocus`
When set to `true`, automatically focuses the search input on page load.

##### `target`
The target to use when opening the search results in a new tab. Possible values are `_blank`, `_self`, `_parent` and `_top`.

##### `placeholder`
When set, modifies the text displayed in the input field before typing.

Expand Down Expand Up @@ -2158,9 +2162,15 @@ Preview:

#### Properties

| Name | Type | Required |
| ---- | ---- | -------- |
| groups | array | yes |
| Name | Type | Required | Default |
| ---- | ---- | -------- | ------- |
| groups | array | yes | |
| style | string | no | list |

##### `style`
The display style of the bookmarks. Possible values are:
- `list` - Traditional list view (default)
- `cards` - Grid of cards with icons and descriptions. Cards automatically adjust their height based on content.

##### `groups`
An array of groups which can optionally have a title and a custom color.
Expand Down
1 change: 1 addition & 0 deletions docs/custom-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,7 @@ The following helper functions provided by Glance are available:
- `offsetNow(offset string) time.Time`: Returns the current time with an offset. The offset can be positive or negative and must be in the format "3h" "-1h" or "2h30m10s".
- `duration(str string) time.Duration`: Parses a string such as `1h`, `24h`, `5h30m`, etc into a `time.Duration`.
- `parseTime(layout string, s string) time.Time`: Parses a string into time.Time. The layout must be provided in Go's [date format](https://pkg.go.dev/time#pkg-constants). You can alternatively use these values instead of the literal format: "unix", "RFC3339", "RFC3339Nano", "DateTime", "DateOnly".
- `parseLocalTime(layout string, s string) time.Time`: Same as the above, except in the absence of a timezone, it will use the local timezone instead of UTC.
- `parseRelativeTime(layout string, s string) time.Time`: A shorthand for `{{ .String "date" | parseTime "rfc3339" | toRelativeTime }}`.
- `add(a, b float) float`: Adds two numbers.
- `sub(a, b float) float`: Subtracts two numbers.
Expand Down
2 changes: 1 addition & 1 deletion internal/glance/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ func cliMountpointInfo(requestedPath string) int {

fmt.Println("Path:", usage.Path)
fmt.Println("FS type:", ternary(usage.Fstype == "", "unknown", usage.Fstype))
fmt.Printf("Used percent: %.1f%%", usage.UsedPercent)
fmt.Printf("Used percent: %.1f%%\n", usage.UsedPercent)

return 0
}
3 changes: 2 additions & 1 deletion internal/glance/static/js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ function setupSearchBoxes() {
for (let i = 0; i < searchWidgets.length; i++) {
const widget = searchWidgets[i];
const defaultSearchUrl = widget.dataset.defaultSearchUrl;
const target = widget.dataset.target || "_blank";
const newTab = widget.dataset.newTab === "true";
const inputElement = widget.getElementsByClassName("search-input")[0];
const bangElement = widget.getElementsByClassName("search-bang")[0];
Expand Down Expand Up @@ -143,7 +144,7 @@ function setupSearchBoxes() {
const url = searchUrlTemplate.replace("!QUERY!", encodeURIComponent(query));

if (newTab && !event.ctrlKey || !newTab && event.ctrlKey) {
window.open(url, '_blank').focus();
window.open(url, target).focus();
} else {
window.location.href = url;
}
Expand Down
30 changes: 30 additions & 0 deletions internal/glance/templates/bookmarks.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,35 @@
{{ template "widget-base.html" . }}

{{ define "widget-content" }}
{{ if eq .Style "cards" }}
<div class="dynamic-columns list-gap-24">
{{ range .Groups }}
<div class="bookmarks-group"{{ if .Color }} style="--bookmarks-group-color: {{ .Color.String | safeCSS }}"{{ end }}>
{{ if ne .Title "" }}<div class="bookmarks-group-title size-h3 margin-bottom-10">{{ .Title }}</div>{{ end }}
<div class="cards-grid">
{{ range .Links }}
<a href="{{ .URL }}" {{ if .Target }}target="{{ .Target }}"{{ end }} rel="noreferrer" class="widget-content-frame padding-widget card flex flex-column items-center">
{{ if ne "" .Icon.URL }}
<div class="bookmarks-icon-container margin-bottom-5" style="padding: 0.8rem;">
<img src="{{ .Icon.URL }}" alt="" class="bookmarks-icon{{ if .Icon.IsFlatIcon }} flat-icon{{ end }}" style="width: 32px; height: 32px;">
</div>
{{ end }}
<h3 class="size-h3 color-primary text-center">{{ .Title }}</h3>
{{ if ne .Description "" }}
<p class="color-text-paragraph text-center text-truncate-2-lines margin-top-3">{{ .Description }}</p>
{{ end }}
{{ if ne .ButtonText "" }}
<div class="margin-top-auto text-center margin-top-7">
<span class="size-h4 color-primary">{{ .ButtonText }}</span>
</div>
{{ end }}
</a>
{{ end }}
</div>
</div>
{{ end }}
</div>
{{ else }}
<div class="dynamic-columns list-gap-24 list-with-separator">
{{- range .Groups }}
<div class="bookmarks-group"{{ if .Color }} style="--bookmarks-group-color: {{ .Color.String | safeCSS }}"{{ end }}>
Expand Down Expand Up @@ -28,3 +57,4 @@
{{- end }}
</div>
{{ end }}
{{ end }}
2 changes: 1 addition & 1 deletion internal/glance/templates/search.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
{{ define "widget-content-classes" }}widget-content-frameless{{ end }}

{{ define "widget-content" }}
<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 widget-content-frame padding-inline-widget flex gap-15 items-center" data-default-search-url="{{ .SearchEngine }}" data-new-tab="{{ .NewTab }}" data-target="{{ .Target }}">
<div class="search-bangs">
{{ range .Bangs }}
<input type="hidden" data-shortcut="{{ .Shortcut }}" data-title="{{ .Title }}" data-url="{{ .URL }}">
Expand Down
2 changes: 2 additions & 0 deletions internal/glance/widget-bookmarks.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ var bookmarksWidgetTemplate = mustParseTemplate("bookmarks.html", "widget-base.h
type bookmarksWidget struct {
widgetBase `yaml:",inline"`
cachedHTML template.HTML `yaml:"-"`
Style string `yaml:"style"`
Groups []struct {
Title string `yaml:"title"`
Color *hslColorField `yaml:"color"`
Expand All @@ -30,6 +31,7 @@ type bookmarksWidget struct {
HideArrowRaw *bool `yaml:"hide-arrow"`
HideArrow bool `yaml:"-"`
Target string `yaml:"target"`
ButtonText string `yaml:"button-text"`
} `yaml:"links"`
} `yaml:"groups"`
}
Expand Down
17 changes: 11 additions & 6 deletions internal/glance/widget-custom-api.go
Original file line number Diff line number Diff line change
Expand Up @@ -454,11 +454,16 @@ var customAPITemplateFuncs = func() template.FuncMap {

return d
},
"parseTime": customAPIFuncParseTime,
"parseTime": func(layout, value string) time.Time {
return customAPIFuncParseTimeInLocation(layout, value, time.UTC)
},
"parseLocalTime": func(layout, value string) time.Time {
return customAPIFuncParseTimeInLocation(layout, value, time.Local)
},
"toRelativeTime": dynamicRelativeTimeAttrs,
"parseRelativeTime": func(layout, value string) template.HTMLAttr {
// Shorthand to do both of the above with a single function call
return dynamicRelativeTimeAttrs(customAPIFuncParseTime(layout, value))
return dynamicRelativeTimeAttrs(customAPIFuncParseTimeInLocation(layout, value, time.UTC))
},
// The reason we flip the parameter order is so that you can chain multiple calls together like this:
// {{ .JSON.String "foo" | trimPrefix "bar" | doSomethingElse }}
Expand Down Expand Up @@ -532,8 +537,8 @@ var customAPITemplateFuncs = func() template.FuncMap {
},
"sortByTime": func(key, layout, order string, results []decoratedGJSONResult) []decoratedGJSONResult {
sort.Slice(results, func(a, b int) bool {
timeA := customAPIFuncParseTime(layout, results[a].String(key))
timeB := customAPIFuncParseTime(layout, results[b].String(key))
timeA := customAPIFuncParseTimeInLocation(layout, results[a].String(key), time.UTC)
timeB := customAPIFuncParseTimeInLocation(layout, results[b].String(key), time.UTC)

if order == "asc" {
return timeA.Before(timeB)
Expand Down Expand Up @@ -570,7 +575,7 @@ var customAPITemplateFuncs = func() template.FuncMap {
return funcs
}()

func customAPIFuncParseTime(layout, value string) time.Time {
func customAPIFuncParseTimeInLocation(layout, value string, loc *time.Location) time.Time {
switch strings.ToLower(layout) {
case "unix":
asInt, err := strconv.ParseInt(value, 10, 64)
Expand All @@ -589,7 +594,7 @@ func customAPIFuncParseTime(layout, value string) time.Time {
layout = time.DateOnly
}

parsed, err := time.Parse(layout, value)
parsed, err := time.ParseInLocation(layout, value, loc)
if err != nil {
return time.Unix(0, 0)
}
Expand Down
2 changes: 1 addition & 1 deletion internal/glance/widget-reddit.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ func fetchSubredditPosts(
var client requestDoer = defaultHTTPClient

if requestUrlTemplate != "" {
requestUrl = strings.ReplaceAll(requestUrlTemplate, "{REQUEST-URL}", requestUrl)
requestUrl = strings.ReplaceAll(requestUrlTemplate, "{REQUEST-URL}", url.QueryEscape(requestUrl))
} else if proxyClient != nil {
client = proxyClient
}
Expand Down
1 change: 1 addition & 0 deletions internal/glance/widget-search.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type searchWidget struct {
SearchEngine string `yaml:"search-engine"`
Bangs []SearchBang `yaml:"bangs"`
NewTab bool `yaml:"new-tab"`
Target string `yaml:"target"`
Autofocus bool `yaml:"autofocus"`
Placeholder string `yaml:"placeholder"`
}
Expand Down
4 changes: 2 additions & 2 deletions internal/glance/widget-utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,8 @@ func workerPoolDo[I any, O any](job *workerPoolJob[I, O]) ([]O, []error, error)
}

if len(job.data) == 1 {
output, err := job.task(job.data[0])
return append(results, output), append(errs, err), nil
results[0], errs[0] = job.task(job.data[0])
return results, errs, nil
}

tasksQueue := make(chan *workerPoolTask[I, O])
Expand Down