main ba1e2c7

favorites.go RawHistoryFolder

1package homepage
2
3import (
4	"context"
5	"encoding/json"
6	"fmt"
7	"strings"
8
9	"github.com/bnema/dumber/internal/application/dto"
10	"github.com/bnema/dumber/internal/application/port"
11	"github.com/bnema/dumber/internal/domain/entity"
12	"github.com/bnema/dumber/internal/logging"
13)
14
15func tagIDsFromInt64s(ids []int64) ([]entity.TagID, error) {
16	out := make([]entity.TagID, 0, len(ids))
17	for _, id := range ids {
18		if id <= 0 {
19			return nil, fmt.Errorf("tag id must be positive")
20		}
21		out = append(out, entity.TagID(id))
22	}
23	return out, nil
24}
25
26// FavoritesHandlers handles favorite-related messages from the homepage.
27type FavoritesHandlers struct {
28	favoritesUC port.HomepageFavorites
29}
30
31// NewFavoritesHandlers creates a new FavoritesHandlers instance.
32func NewFavoritesHandlers(favoritesUC port.HomepageFavorites) *FavoritesHandlers {
33	return &FavoritesHandlers{favoritesUC: favoritesUC}
34}
35
36// HandleList handles favorite_list messages.
37func (h *FavoritesHandlers) HandleList() port.WebUIMessageHandler {
38	return port.WebUIMessageHandlerFunc(func(ctx context.Context, _ port.WebViewID, payload json.RawMessage) (any, error) {
39		log := logging.FromContext(ctx)
40
41		requestID := ParseRequestID(payload)
42
43		log.Debug().
44			Str("request_id", requestID).
45			Msg("handling favorite_list")
46
47		favorites, err := h.favoritesUC.GetAll(ctx)
48		if err != nil {
49			return NewErrorResponse(requestID, err), nil
50		}
51
52		return NewSuccessResponse(requestID, favorites), nil
53	})
54}
55
56type favoriteCreateRequest struct {
57	RequestID  string  `json:"requestId"`
58	URL        string  `json:"url"`
59	Title      string  `json:"title"`
60	FaviconURL string  `json:"favicon_url"`
61	Tags       []int64 `json:"tags"`
62}
63
64// HandleCreate handles favorite_create messages.
65func (h *FavoritesHandlers) HandleCreate() port.WebUIMessageHandler {
66	return port.WebUIMessageHandlerFunc(func(ctx context.Context, _ port.WebViewID, payload json.RawMessage) (any, error) {
67		log := logging.FromContext(ctx)
68
69		var req favoriteCreateRequest
70		if err := json.Unmarshal(payload, &req); err != nil {
71			return NewErrorResponse("", err), nil
72		}
73
74		log.Debug().
75			Str("request_id", req.RequestID).
76			Msg("handling favorite_create")
77
78		trimmedURL := strings.TrimSpace(req.URL)
79		if trimmedURL == "" {
80			return NewErrorResponse(req.RequestID, fmt.Errorf("URL is required")), nil
81		}
82		tags, err := tagIDsFromInt64s(req.Tags)
83		if err != nil {
84			return NewErrorResponse(req.RequestID, err), nil
85		}
86
87		input := dto.FavoriteCreateInput{
88			URL:        trimmedURL,
89			Title:      req.Title,
90			FaviconURL: req.FaviconURL,
91			Tags:       tags,
92		}
93		favorite, err := h.favoritesUC.AddFavorite(ctx, input)
94		if err != nil {
95			return NewErrorResponse(req.RequestID, err), nil
96		}
97
98		return NewSuccessResponse(req.RequestID, favorite), nil
99	})
100}
101
102type favoriteUpdateRequest struct {
103	RequestID   string `json:"requestId"`
104	ID          int64  `json:"id"`
105	Title       string `json:"title"`
106	FaviconURL  string `json:"favicon_url"`
107	ShortcutKey *int   `json:"shortcut_key"`
108}
109
110// HandleUpdate handles favorite_update messages.
111func (h *FavoritesHandlers) HandleUpdate() port.WebUIMessageHandler {
112	return port.WebUIMessageHandlerFunc(func(ctx context.Context, _ port.WebViewID, payload json.RawMessage) (any, error) {
113		log := logging.FromContext(ctx)
114
115		var req favoriteUpdateRequest
116		if err := json.Unmarshal(payload, &req); err != nil {
117			return NewErrorResponse("", err), nil
118		}
119		var fields map[string]json.RawMessage
120		if err := json.Unmarshal(payload, &fields); err != nil {
121			return NewErrorResponse("", err), nil
122		}
123		_, shortcutKeySet := fields["shortcut_key"]
124
125		log.Debug().
126			Str("request_id", req.RequestID).
127			Int64("favorite_id", req.ID).
128			Msg("handling favorite_update")
129
130		if req.ID <= 0 {
131			return NewErrorResponse(req.RequestID, fmt.Errorf("favorite id must be positive")), nil
132		}
133
134		favorite, err := h.favoritesUC.UpdateFavorite(ctx, dto.FavoriteUpdateInput{
135			ID:             entity.FavoriteID(req.ID),
136			Title:          req.Title,
137			FaviconURL:     req.FaviconURL,
138			ShortcutKey:    req.ShortcutKey,
139			ShortcutKeySet: shortcutKeySet,
140		})
141		if err != nil {
142			return NewErrorResponse(req.RequestID, err), nil
143		}
144
145		return NewSuccessResponse(req.RequestID, favorite), nil
146	})
147}
148
149type favoriteDeleteRequest struct {
150	RequestID string `json:"requestId"`
151	ID        int64  `json:"id"`
152}
153
154// HandleDelete handles favorite_delete messages.
155func (h *FavoritesHandlers) HandleDelete() port.WebUIMessageHandler {
156	return port.WebUIMessageHandlerFunc(func(ctx context.Context, _ port.WebViewID, payload json.RawMessage) (any, error) {
157		log := logging.FromContext(ctx)
158
159		var req favoriteDeleteRequest
160		if err := json.Unmarshal(payload, &req); err != nil {
161			return NewErrorResponse("", err), nil
162		}
163
164		log.Debug().
165			Str("request_id", req.RequestID).
166			Int64("favorite_id", req.ID).
167			Msg("handling favorite_delete")
168
169		if req.ID <= 0 {
170			return NewErrorResponse(req.RequestID, fmt.Errorf("favorite id must be positive")), nil
171		}
172
173		if err := h.favoritesUC.DeleteFavorite(ctx, entity.FavoriteID(req.ID)); err != nil {
174			return NewErrorResponse(req.RequestID, err), nil
175		}
176
177		return NewSuccessResponse(req.RequestID, nil), nil
178	})
179}
180
181// setShortcutRequest is the payload for favorite_set_shortcut messages.
182type setShortcutRequest struct {
183	RequestID   string `json:"requestId"`
184	FavoriteID  int64  `json:"favorite_id"`
185	ShortcutKey *int   `json:"shortcut_key"` // 1-9 or null to remove
186}
187
188// HandleSetShortcut handles favorite_set_shortcut messages.
189func (h *FavoritesHandlers) HandleSetShortcut() port.WebUIMessageHandler {
190	return port.WebUIMessageHandlerFunc(func(ctx context.Context, _ port.WebViewID, payload json.RawMessage) (any, error) {
191		log := logging.FromContext(ctx)
192
193		var req setShortcutRequest
194		if err := json.Unmarshal(payload, &req); err != nil {
195			return NewErrorResponse("", err), nil
196		}
197
198		log.Debug().
199			Str("request_id", req.RequestID).
200			Int64("favorite_id", req.FavoriteID).
201			Msg("handling favorite_set_shortcut")
202
203		if req.FavoriteID <= 0 {
204			return NewErrorResponse(req.RequestID, fmt.Errorf("invalid favorite id")), nil
205		}
206
207		if err := h.favoritesUC.SetShortcut(ctx, entity.FavoriteID(req.FavoriteID), req.ShortcutKey); err != nil {
208			return NewErrorResponse(req.RequestID, err), nil
209		}
210
211		return NewSuccessResponse(req.RequestID, nil), nil
212	})
213}
214
215// getByShortcutRequest is the payload for favorite_get_by_shortcut messages.
216type getByShortcutRequest struct {
217	RequestID   string `json:"requestId"`
218	ShortcutKey int    `json:"shortcut_key"`
219}
220
221// HandleGetByShortcut handles favorite_get_by_shortcut messages.
222func (h *FavoritesHandlers) HandleGetByShortcut() port.WebUIMessageHandler {
223	return port.WebUIMessageHandlerFunc(func(ctx context.Context, _ port.WebViewID, payload json.RawMessage) (any, error) {
224		log := logging.FromContext(ctx)
225
226		var req getByShortcutRequest
227		if err := json.Unmarshal(payload, &req); err != nil {
228			return NewErrorResponse("", err), nil
229		}
230
231		log.Debug().
232			Str("request_id", req.RequestID).
233			Int("shortcut_key", req.ShortcutKey).
234			Msg("handling favorite_get_by_shortcut")
235
236		favorite, err := h.favoritesUC.GetByShortcut(ctx, req.ShortcutKey)
237		if err != nil {
238			return NewErrorResponse(req.RequestID, err), nil
239		}
240
241		return NewSuccessResponse(req.RequestID, favorite), nil
242	})
243}

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