v0.29.0 21b3150

favorites_view.go RawHistoryFolder

1package systemviews
2
3import (
4	"fmt"
5	"strings"
6
7	"github.com/bnema/dumber/internal/domain/entity"
8)
9
10const defaultTagColor = "#808080"
11
12type favoritesRenderData struct {
13	Favorites    []*entity.Favorite
14	Folders      []*entity.Folder
15	Tags         []*entity.Tag
16	FolderFilter *entity.FolderID
17	TagFilter    *entity.TagID
18	Notice       string
19	Error        string
20}
21
22func favoritesHTML(data favoritesRenderData) string {
23	return mustRenderComponent(FavoritesView(data))
24}
25
26func favoritesDocumentTitle(data favoritesRenderData) string {
27	count := len(data.Favorites)
28	if data.FolderFilter != nil || data.TagFilter != nil {
29		if count == 1 {
30			return "Favorites — 1 filtered bookmark"
31		}
32		return fmt.Sprintf("Favorites — %d filtered bookmarks", count)
33	}
34	if count == 1 {
35		return "Favorites — 1 bookmark"
36	}
37	return fmt.Sprintf("Favorites — %d bookmarks", count)
38}
39
40func favoritesSummary(data favoritesRenderData) string {
41	return fmt.Sprintf("%d favorites · %d folders · %d tags", len(data.Favorites), len(data.Folders), len(data.Tags))
42}
43
44func foldersSummary(data favoritesRenderData) string {
45	return fmt.Sprintf("%d folders", len(data.Folders))
46}
47
48func tagsSummary(data favoritesRenderData) string {
49	return fmt.Sprintf("%d tags", len(data.Tags))
50}
51
52func favoriteFilterClass(active bool) string {
53	classes := "sv-button sv-button-secondary"
54	if active {
55		classes += " sv-button-active"
56	}
57	return classes
58}
59
60func favoriteFolderFilterActive(data favoritesRenderData, folderID entity.FolderID) bool {
61	return data.FolderFilter != nil && *data.FolderFilter == folderID
62}
63
64func favoriteAllFilterActive(data favoritesRenderData) bool {
65	return data.FolderFilter == nil && data.TagFilter == nil
66}
67
68func favoriteTagFilterActive(data favoritesRenderData, tagID entity.TagID) bool {
69	return data.TagFilter != nil && *data.TagFilter == tagID
70}
71
72func favoriteItemLabel(favorite *entity.Favorite) string {
73	if favorite == nil {
74		return ""
75	}
76	label := strings.TrimSpace(favorite.Title)
77	if label == "" {
78		label = favorite.URL
79	}
80	return label
81}
82
83func favoriteMetaText(favorite *entity.Favorite, folders []*entity.Folder) string {
84	if favorite == nil {
85		return ""
86	}
87	parts := make([]string, 0, 2)
88	if favorite.ShortcutKey != nil {
89		parts = append(parts, fmt.Sprintf("Shortcut %d", *favorite.ShortcutKey))
90	}
91	if name := folderNameForID(folders, favorite.FolderID); name != "" {
92		parts = append(parts, "Folder "+name)
93	}
94	return strings.Join(parts, " · ")
95}
96
97func favoriteTagButtonAction(favorite *entity.Favorite, tag *entity.Tag) string {
98	if favorite != nil && tag != nil && favorite.HasTag(tag.ID) {
99		return tagActionRemove
100	}
101	return tagActionAssign
102}
103
104func favoriteTagButtonLabel(favorite *entity.Favorite, tag *entity.Tag) string {
105	if tag == nil {
106		return ""
107	}
108	if favorite != nil && favorite.HasTag(tag.ID) {
109		return "✓ " + tag.Name
110	}
111	return "+ " + tag.Name
112}
113
114func favoriteTagButtonClass(favorite *entity.Favorite, tag *entity.Tag) string {
115	classes := "sv-button sv-button-secondary sv-tag-button"
116	if favorite != nil && tag != nil && favorite.HasTag(tag.ID) {
117		classes += " sv-button-active sv-tag-button-active"
118	}
119	return classes
120}
121
122func safeTagColor(raw string) string {
123	value := strings.TrimSpace(raw)
124	if len(value) != 4 && len(value) != 7 {
125		return defaultTagColor
126	}
127	if value[0] != '#' {
128		return defaultTagColor
129	}
130	for _, ch := range value[1:] {
131		if (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F') {
132			continue
133		}
134		return defaultTagColor
135	}
136	return value
137}
138
139func folderDisplayName(folder *entity.Folder) string {
140	if folder == nil {
141		return ""
142	}
143	if strings.TrimSpace(folder.Icon) != "" {
144		return folder.Icon + " " + folder.Name
145	}
146	return folder.Name
147}
148
149func folderNameForID(folders []*entity.Folder, id *entity.FolderID) string {
150	if id == nil {
151		return "Unfiled"
152	}
153	for _, folder := range folders {
154		if folder != nil && folder.ID == *id {
155			return folderDisplayName(folder)
156		}
157	}
158	return "Unknown"
159}
160
161func folderSelected(selected *entity.FolderID, folderID entity.FolderID) bool {
162	return selected != nil && *selected == folderID
163}
164
165func shortcutSelected(selected *int, value int) bool {
166	return selected != nil && *selected == value
167}
168
169func filterFavorites(favorites []*entity.Favorite, folderID *entity.FolderID, tagID *entity.TagID) []*entity.Favorite {
170	if folderID == nil && tagID == nil {
171		return favorites
172	}
173	out := make([]*entity.Favorite, 0, len(favorites))
174	for _, favorite := range favorites {
175		if favorite == nil {
176			continue
177		}
178		if folderID != nil {
179			if *folderID == 0 {
180				if favorite.FolderID != nil {
181					continue
182				}
183			} else if favorite.FolderID == nil || *favorite.FolderID != *folderID {
184				continue
185			}
186		}
187		if tagID != nil && !favorite.HasTag(*tagID) {
188			continue
189		}
190		out = append(out, favorite)
191	}
192	return out
193}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close