detached f42505b

feat(handlers): implement homepage message handlers for history, favorites, folders, tags

commit
f42505b50422e2b3f77839490cf11c07415ac08b
author
brice · · 2025-12-16 17:34 UTC
committer
brice · · 2025-12-16 17:15 UTC
parent
d2e410e
signature
unsigned
7 files · +933 -0
A internal/infrastructure/webkit/handlers/homepage/favorites.go +137 -0
@@ -0,0 +1,137 @@
1+package homepage
2+
3+import (
4+ "context"
5+ "encoding/json"
6+
7+ "github.com/bnema/dumber/internal/application/usecase"
8+ "github.com/bnema/dumber/internal/domain/entity"
9+ "github.com/bnema/dumber/internal/infrastructure/webkit"
10+ "github.com/bnema/dumber/internal/logging"
11+)
12+
13+// FavoritesHandlers handles favorite-related messages from the homepage.
14+type FavoritesHandlers struct {
15+ favoritesUC *usecase.ManageFavoritesUseCase
16+}
17+
18+// NewFavoritesHandlers creates a new FavoritesHandlers instance.
19+func NewFavoritesHandlers(favoritesUC *usecase.ManageFavoritesUseCase) *FavoritesHandlers {
20+ return &FavoritesHandlers{favoritesUC: favoritesUC}
21+}
22+
23+// HandleList handles favorite_list messages.
24+func (h *FavoritesHandlers) HandleList() webkit.MessageHandler {
25+ return webkit.MessageHandlerFunc(func(ctx context.Context, webviewID webkit.WebViewID, payload json.RawMessage) (any, error) {
26+ log := logging.FromContext(ctx)
27+
28+ requestID := ParseRequestID(payload)
29+
30+ log.Debug().
31+ Str("request_id", requestID).
32+ Msg("handling favorite_list")
33+
34+ favorites, err := h.favoritesUC.GetAll(ctx)
35+ if err != nil {
36+ return NewErrorResponse(requestID, err), nil
37+ }
38+
39+ return NewSuccessResponse(requestID, favorites), nil
40+ })
41+}
42+
43+// setShortcutRequest is the payload for favorite_set_shortcut messages.
44+type setShortcutRequest struct {
45+ RequestID string `json:"requestId"`
46+ FavoriteID int64 `json:"favorite_id"`
47+ ShortcutKey *int `json:"shortcut_key"` // 1-9 or null to remove
48+}
49+
50+// HandleSetShortcut handles favorite_set_shortcut messages.
51+func (h *FavoritesHandlers) HandleSetShortcut() webkit.MessageHandler {
52+ return webkit.MessageHandlerFunc(func(ctx context.Context, webviewID webkit.WebViewID, payload json.RawMessage) (any, error) {
53+ log := logging.FromContext(ctx)
54+
55+ var req setShortcutRequest
56+ if err := json.Unmarshal(payload, &req); err != nil {
57+ return NewErrorResponse("", err), nil
58+ }
59+
60+ log.Debug().
61+ Str("request_id", req.RequestID).
62+ Int64("favorite_id", req.FavoriteID).
63+ Msg("handling favorite_set_shortcut")
64+
65+ if err := h.favoritesUC.SetShortcut(ctx, entity.FavoriteID(req.FavoriteID), req.ShortcutKey); err != nil {
66+ return NewErrorResponse(req.RequestID, err), nil
67+ }
68+
69+ return NewSuccessResponse(req.RequestID, nil), nil
70+ })
71+}
72+
73+// getByShortcutRequest is the payload for favorite_get_by_shortcut messages.
74+type getByShortcutRequest struct {
75+ RequestID string `json:"requestId"`
76+ ShortcutKey int `json:"shortcut_key"`
77+}
78+
79+// HandleGetByShortcut handles favorite_get_by_shortcut messages.
80+func (h *FavoritesHandlers) HandleGetByShortcut() webkit.MessageHandler {
81+ return webkit.MessageHandlerFunc(func(ctx context.Context, webviewID webkit.WebViewID, payload json.RawMessage) (any, error) {
82+ log := logging.FromContext(ctx)
83+
84+ var req getByShortcutRequest
85+ if err := json.Unmarshal(payload, &req); err != nil {
86+ return NewErrorResponse("", err), nil
87+ }
88+
89+ log.Debug().
90+ Str("request_id", req.RequestID).
91+ Int("shortcut_key", req.ShortcutKey).
92+ Msg("handling favorite_get_by_shortcut")
93+
94+ favorite, err := h.favoritesUC.GetByShortcut(ctx, req.ShortcutKey)
95+ if err != nil {
96+ return NewErrorResponse(req.RequestID, err), nil
97+ }
98+
99+ return NewSuccessResponse(req.RequestID, favorite), nil
100+ })
101+}
102+
103+// setFolderRequest is the payload for favorite_set_folder messages.
104+type setFolderRequest struct {
105+ RequestID string `json:"requestId"`
106+ FavoriteID int64 `json:"favorite_id"`
107+ FolderID *int64 `json:"folder_id"` // null to move to root
108+}
109+
110+// HandleSetFolder handles favorite_set_folder messages.
111+func (h *FavoritesHandlers) HandleSetFolder() webkit.MessageHandler {
112+ return webkit.MessageHandlerFunc(func(ctx context.Context, webviewID webkit.WebViewID, payload json.RawMessage) (any, error) {
113+ log := logging.FromContext(ctx)
114+
115+ var req setFolderRequest
116+ if err := json.Unmarshal(payload, &req); err != nil {
117+ return NewErrorResponse("", err), nil
118+ }
119+
120+ log.Debug().
121+ Str("request_id", req.RequestID).
122+ Int64("favorite_id", req.FavoriteID).
123+ Msg("handling favorite_set_folder")
124+
125+ var folderID *entity.FolderID
126+ if req.FolderID != nil {
127+ id := entity.FolderID(*req.FolderID)
128+ folderID = &id
129+ }
130+
131+ if err := h.favoritesUC.Move(ctx, entity.FavoriteID(req.FavoriteID), folderID); err != nil {
132+ return NewErrorResponse(req.RequestID, err), nil
133+ }
134+
135+ return NewSuccessResponse(req.RequestID, nil), nil
136+ })
137+}
@@ -0,0 +1,137 @@
1+package homepage
2+
3+import (
4+ "context"
5+ "encoding/json"
6+
7+ "github.com/bnema/dumber/internal/application/usecase"
8+ "github.com/bnema/dumber/internal/domain/entity"
9+ "github.com/bnema/dumber/internal/infrastructure/webkit"
10+ "github.com/bnema/dumber/internal/logging"
11+)
12+
13+// FavoritesHandlers handles favorite-related messages from the homepage.
14+type FavoritesHandlers struct {
15+ favoritesUC *usecase.ManageFavoritesUseCase
16+}
17+
18+// NewFavoritesHandlers creates a new FavoritesHandlers instance.
19+func NewFavoritesHandlers(favoritesUC *usecase.ManageFavoritesUseCase) *FavoritesHandlers {
20+ return &FavoritesHandlers{favoritesUC: favoritesUC}
21+}
22+
23+// HandleList handles favorite_list messages.
24+func (h *FavoritesHandlers) HandleList() webkit.MessageHandler {
25+ return webkit.MessageHandlerFunc(func(ctx context.Context, webviewID webkit.WebViewID, payload json.RawMessage) (any, error) {
26+ log := logging.FromContext(ctx)
27+
28+ requestID := ParseRequestID(payload)
29+
30+ log.Debug().
31+ Str("request_id", requestID).
32+ Msg("handling favorite_list")
33+
34+ favorites, err := h.favoritesUC.GetAll(ctx)
35+ if err != nil {
36+ return NewErrorResponse(requestID, err), nil
37+ }
38+
39+ return NewSuccessResponse(requestID, favorites), nil
40+ })
41+}
42+
43+// setShortcutRequest is the payload for favorite_set_shortcut messages.
44+type setShortcutRequest struct {
45+ RequestID string `json:"requestId"`
46+ FavoriteID int64 `json:"favorite_id"`
47+ ShortcutKey *int `json:"shortcut_key"` // 1-9 or null to remove
48+}
49+
50+// HandleSetShortcut handles favorite_set_shortcut messages.
51+func (h *FavoritesHandlers) HandleSetShortcut() webkit.MessageHandler {
52+ return webkit.MessageHandlerFunc(func(ctx context.Context, webviewID webkit.WebViewID, payload json.RawMessage) (any, error) {
53+ log := logging.FromContext(ctx)
54+
55+ var req setShortcutRequest
56+ if err := json.Unmarshal(payload, &req); err != nil {
57+ return NewErrorResponse("", err), nil
58+ }
59+
60+ log.Debug().
61+ Str("request_id", req.RequestID).
62+ Int64("favorite_id", req.FavoriteID).
63+ Msg("handling favorite_set_shortcut")
64+
65+ if err := h.favoritesUC.SetShortcut(ctx, entity.FavoriteID(req.FavoriteID), req.ShortcutKey); err != nil {
66+ return NewErrorResponse(req.RequestID, err), nil
67+ }
68+
69+ return NewSuccessResponse(req.RequestID, nil), nil
70+ })
71+}
72+
73+// getByShortcutRequest is the payload for favorite_get_by_shortcut messages.
74+type getByShortcutRequest struct {
75+ RequestID string `json:"requestId"`
76+ ShortcutKey int `json:"shortcut_key"`
77+}
78+
79+// HandleGetByShortcut handles favorite_get_by_shortcut messages.
80+func (h *FavoritesHandlers) HandleGetByShortcut() webkit.MessageHandler {
81+ return webkit.MessageHandlerFunc(func(ctx context.Context, webviewID webkit.WebViewID, payload json.RawMessage) (any, error) {
82+ log := logging.FromContext(ctx)
83+
84+ var req getByShortcutRequest
85+ if err := json.Unmarshal(payload, &req); err != nil {
86+ return NewErrorResponse("", err), nil
87+ }
88+
89+ log.Debug().
90+ Str("request_id", req.RequestID).
91+ Int("shortcut_key", req.ShortcutKey).
92+ Msg("handling favorite_get_by_shortcut")
93+
94+ favorite, err := h.favoritesUC.GetByShortcut(ctx, req.ShortcutKey)
95+ if err != nil {
96+ return NewErrorResponse(req.RequestID, err), nil
97+ }
98+
99+ return NewSuccessResponse(req.RequestID, favorite), nil
100+ })
101+}
102+
103+// setFolderRequest is the payload for favorite_set_folder messages.
104+type setFolderRequest struct {
105+ RequestID string `json:"requestId"`
106+ FavoriteID int64 `json:"favorite_id"`
107+ FolderID *int64 `json:"folder_id"` // null to move to root
108+}
109+
110+// HandleSetFolder handles favorite_set_folder messages.
111+func (h *FavoritesHandlers) HandleSetFolder() webkit.MessageHandler {
112+ return webkit.MessageHandlerFunc(func(ctx context.Context, webviewID webkit.WebViewID, payload json.RawMessage) (any, error) {
113+ log := logging.FromContext(ctx)
114+
115+ var req setFolderRequest
116+ if err := json.Unmarshal(payload, &req); err != nil {
117+ return NewErrorResponse("", err), nil
118+ }
119+
120+ log.Debug().
121+ Str("request_id", req.RequestID).
122+ Int64("favorite_id", req.FavoriteID).
123+ Msg("handling favorite_set_folder")
124+
125+ var folderID *entity.FolderID
126+ if req.FolderID != nil {
127+ id := entity.FolderID(*req.FolderID)
128+ folderID = &id
129+ }
130+
131+ if err := h.favoritesUC.Move(ctx, entity.FavoriteID(req.FavoriteID), folderID); err != nil {
132+ return NewErrorResponse(req.RequestID, err), nil
133+ }
134+
135+ return NewSuccessResponse(req.RequestID, nil), nil
136+ })
137+}
A internal/infrastructure/webkit/handlers/homepage/folders.go +144 -0
@@ -0,0 +1,144 @@
1+package homepage
2+
3+import (
4+ "context"
5+ "encoding/json"
6+
7+ "github.com/bnema/dumber/internal/application/usecase"
8+ "github.com/bnema/dumber/internal/domain/entity"
9+ "github.com/bnema/dumber/internal/infrastructure/webkit"
10+ "github.com/bnema/dumber/internal/logging"
11+)
12+
13+// FolderHandlers handles folder-related messages from the homepage.
14+type FolderHandlers struct {
15+ favoritesUC *usecase.ManageFavoritesUseCase
16+}
17+
18+// NewFolderHandlers creates a new FolderHandlers instance.
19+func NewFolderHandlers(favoritesUC *usecase.ManageFavoritesUseCase) *FolderHandlers {
20+ return &FolderHandlers{favoritesUC: favoritesUC}
21+}
22+
23+// HandleList handles folder_list messages.
24+func (h *FolderHandlers) HandleList() webkit.MessageHandler {
25+ return webkit.MessageHandlerFunc(func(ctx context.Context, webviewID webkit.WebViewID, payload json.RawMessage) (any, error) {
26+ log := logging.FromContext(ctx)
27+
28+ requestID := ParseRequestID(payload)
29+
30+ log.Debug().
31+ Str("request_id", requestID).
32+ Msg("handling folder_list")
33+
34+ folders, err := h.favoritesUC.GetAllFolders(ctx)
35+ if err != nil {
36+ return NewErrorResponse(requestID, err), nil
37+ }
38+
39+ return NewSuccessResponse(requestID, folders), nil
40+ })
41+}
42+
43+// createFolderRequest is the payload for folder_create messages.
44+type createFolderRequest struct {
45+ RequestID string `json:"requestId"`
46+ Name string `json:"name"`
47+ Icon *string `json:"icon"`
48+}
49+
50+// HandleCreate handles folder_create messages.
51+func (h *FolderHandlers) HandleCreate() webkit.MessageHandler {
52+ return webkit.MessageHandlerFunc(func(ctx context.Context, webviewID webkit.WebViewID, payload json.RawMessage) (any, error) {
53+ log := logging.FromContext(ctx)
54+
55+ var req createFolderRequest
56+ if err := json.Unmarshal(payload, &req); err != nil {
57+ return NewErrorResponse("", err), nil
58+ }
59+
60+ log.Debug().
61+ Str("request_id", req.RequestID).
62+ Str("name", req.Name).
63+ Msg("handling folder_create")
64+
65+ folder, err := h.favoritesUC.CreateFolder(ctx, req.Name, nil)
66+ if err != nil {
67+ return NewErrorResponse(req.RequestID, err), nil
68+ }
69+
70+ // Set icon if provided
71+ if req.Icon != nil && *req.Icon != "" {
72+ folder.Icon = *req.Icon
73+ }
74+
75+ return NewSuccessResponse(req.RequestID, folder), nil
76+ })
77+}
78+
79+// deleteFolderRequest is the payload for folder_delete messages.
80+type deleteFolderRequest struct {
81+ RequestID string `json:"requestId"`
82+ ID int64 `json:"id"`
83+}
84+
85+// HandleDelete handles folder_delete messages.
86+func (h *FolderHandlers) HandleDelete() webkit.MessageHandler {
87+ return webkit.MessageHandlerFunc(func(ctx context.Context, webviewID webkit.WebViewID, payload json.RawMessage) (any, error) {
88+ log := logging.FromContext(ctx)
89+
90+ var req deleteFolderRequest
91+ if err := json.Unmarshal(payload, &req); err != nil {
92+ return NewErrorResponse("", err), nil
93+ }
94+
95+ log.Debug().
96+ Str("request_id", req.RequestID).
97+ Int64("id", req.ID).
98+ Msg("handling folder_delete")
99+
100+ if err := h.favoritesUC.DeleteFolder(ctx, entity.FolderID(req.ID)); err != nil {
101+ return NewErrorResponse(req.RequestID, err), nil
102+ }
103+
104+ return NewSuccessResponse(req.RequestID, nil), nil
105+ })
106+}
107+
108+// updateFolderRequest is the payload for folder_update messages.
109+type updateFolderRequest struct {
110+ RequestID string `json:"requestId"`
111+ ID int64 `json:"id"`
112+ Name string `json:"name"`
113+ Icon *string `json:"icon"`
114+}
115+
116+// HandleUpdate handles folder_update messages.
117+// NOTE: This requires UpdateFolder() method to be added to ManageFavoritesUseCase.
118+func (h *FolderHandlers) HandleUpdate() webkit.MessageHandler {
119+ return webkit.MessageHandlerFunc(func(ctx context.Context, webviewID webkit.WebViewID, payload json.RawMessage) (any, error) {
120+ log := logging.FromContext(ctx)
121+
122+ var req updateFolderRequest
123+ if err := json.Unmarshal(payload, &req); err != nil {
124+ return NewErrorResponse("", err), nil
125+ }
126+
127+ log.Debug().
128+ Str("request_id", req.RequestID).
129+ Int64("id", req.ID).
130+ Str("name", req.Name).
131+ Msg("handling folder_update")
132+
133+ icon := ""
134+ if req.Icon != nil {
135+ icon = *req.Icon
136+ }
137+
138+ if err := h.favoritesUC.UpdateFolder(ctx, entity.FolderID(req.ID), req.Name, icon); err != nil {
139+ return NewErrorResponse(req.RequestID, err), nil
140+ }
141+
142+ return NewSuccessResponse(req.RequestID, nil), nil
143+ })
144+}
@@ -0,0 +1,144 @@
1+package homepage
2+
3+import (
4+ "context"
5+ "encoding/json"
6+
7+ "github.com/bnema/dumber/internal/application/usecase"
8+ "github.com/bnema/dumber/internal/domain/entity"
9+ "github.com/bnema/dumber/internal/infrastructure/webkit"
10+ "github.com/bnema/dumber/internal/logging"
11+)
12+
13+// FolderHandlers handles folder-related messages from the homepage.
14+type FolderHandlers struct {
15+ favoritesUC *usecase.ManageFavoritesUseCase
16+}
17+
18+// NewFolderHandlers creates a new FolderHandlers instance.
19+func NewFolderHandlers(favoritesUC *usecase.ManageFavoritesUseCase) *FolderHandlers {
20+ return &FolderHandlers{favoritesUC: favoritesUC}
21+}
22+
23+// HandleList handles folder_list messages.
24+func (h *FolderHandlers) HandleList() webkit.MessageHandler {
25+ return webkit.MessageHandlerFunc(func(ctx context.Context, webviewID webkit.WebViewID, payload json.RawMessage) (any, error) {
26+ log := logging.FromContext(ctx)
27+
28+ requestID := ParseRequestID(payload)
29+
30+ log.Debug().
31+ Str("request_id", requestID).
32+ Msg("handling folder_list")
33+
34+ folders, err := h.favoritesUC.GetAllFolders(ctx)
35+ if err != nil {
36+ return NewErrorResponse(requestID, err), nil
37+ }
38+
39+ return NewSuccessResponse(requestID, folders), nil
40+ })
41+}
42+
43+// createFolderRequest is the payload for folder_create messages.
44+type createFolderRequest struct {
45+ RequestID string `json:"requestId"`
46+ Name string `json:"name"`
47+ Icon *string `json:"icon"`
48+}
49+
50+// HandleCreate handles folder_create messages.
51+func (h *FolderHandlers) HandleCreate() webkit.MessageHandler {
52+ return webkit.MessageHandlerFunc(func(ctx context.Context, webviewID webkit.WebViewID, payload json.RawMessage) (any, error) {
53+ log := logging.FromContext(ctx)
54+
55+ var req createFolderRequest
56+ if err := json.Unmarshal(payload, &req); err != nil {
57+ return NewErrorResponse("", err), nil
58+ }
59+
60+ log.Debug().
61+ Str("request_id", req.RequestID).
62+ Str("name", req.Name).
63+ Msg("handling folder_create")
64+
65+ folder, err := h.favoritesUC.CreateFolder(ctx, req.Name, nil)
66+ if err != nil {
67+ return NewErrorResponse(req.RequestID, err), nil
68+ }
69+
70+ // Set icon if provided
71+ if req.Icon != nil && *req.Icon != "" {
72+ folder.Icon = *req.Icon
73+ }
74+
75+ return NewSuccessResponse(req.RequestID, folder), nil
76+ })
77+}
78+
79+// deleteFolderRequest is the payload for folder_delete messages.
80+type deleteFolderRequest struct {
81+ RequestID string `json:"requestId"`
82+ ID int64 `json:"id"`
83+}
84+
85+// HandleDelete handles folder_delete messages.
86+func (h *FolderHandlers) HandleDelete() webkit.MessageHandler {
87+ return webkit.MessageHandlerFunc(func(ctx context.Context, webviewID webkit.WebViewID, payload json.RawMessage) (any, error) {
88+ log := logging.FromContext(ctx)
89+
90+ var req deleteFolderRequest
91+ if err := json.Unmarshal(payload, &req); err != nil {
92+ return NewErrorResponse("", err), nil
93+ }
94+
95+ log.Debug().
96+ Str("request_id", req.RequestID).
97+ Int64("id", req.ID).
98+ Msg("handling folder_delete")
99+
100+ if err := h.favoritesUC.DeleteFolder(ctx, entity.FolderID(req.ID)); err != nil {
101+ return NewErrorResponse(req.RequestID, err), nil
102+ }
103+
104+ return NewSuccessResponse(req.RequestID, nil), nil
105+ })
106+}
107+
108+// updateFolderRequest is the payload for folder_update messages.
109+type updateFolderRequest struct {
110+ RequestID string `json:"requestId"`
111+ ID int64 `json:"id"`
112+ Name string `json:"name"`
113+ Icon *string `json:"icon"`
114+}
115+
116+// HandleUpdate handles folder_update messages.
117+// NOTE: This requires UpdateFolder() method to be added to ManageFavoritesUseCase.
118+func (h *FolderHandlers) HandleUpdate() webkit.MessageHandler {
119+ return webkit.MessageHandlerFunc(func(ctx context.Context, webviewID webkit.WebViewID, payload json.RawMessage) (any, error) {
120+ log := logging.FromContext(ctx)
121+
122+ var req updateFolderRequest
123+ if err := json.Unmarshal(payload, &req); err != nil {
124+ return NewErrorResponse("", err), nil
125+ }
126+
127+ log.Debug().
128+ Str("request_id", req.RequestID).
129+ Int64("id", req.ID).
130+ Str("name", req.Name).
131+ Msg("handling folder_update")
132+
133+ icon := ""
134+ if req.Icon != nil {
135+ icon = *req.Icon
136+ }
137+
138+ if err := h.favoritesUC.UpdateFolder(ctx, entity.FolderID(req.ID), req.Name, icon); err != nil {
139+ return NewErrorResponse(req.RequestID, err), nil
140+ }
141+
142+ return NewSuccessResponse(req.RequestID, nil), nil
143+ })
144+}
A internal/infrastructure/webkit/handlers/homepage/history.go +278 -0
@@ -0,0 +1,278 @@
1+package homepage
2+
3+import (
4+ "context"
5+ "encoding/json"
6+ "time"
7+
8+ "github.com/bnema/dumber/internal/application/usecase"
9+ "github.com/bnema/dumber/internal/domain/entity"
10+ "github.com/bnema/dumber/internal/infrastructure/webkit"
11+ "github.com/bnema/dumber/internal/logging"
12+)
13+
14+// HistoryHandlers handles history-related messages from the homepage.
15+type HistoryHandlers struct {
16+ historyUC *usecase.SearchHistoryUseCase
17+}
18+
19+// NewHistoryHandlers creates a new HistoryHandlers instance.
20+func NewHistoryHandlers(historyUC *usecase.SearchHistoryUseCase) *HistoryHandlers {
21+ return &HistoryHandlers{historyUC: historyUC}
22+}
23+
24+// timelineRequest is the payload for history_timeline messages.
25+type timelineRequest struct {
26+ RequestID string `json:"requestId"`
27+ Limit int `json:"limit"`
28+ Offset int `json:"offset"`
29+}
30+
31+// HandleTimeline handles history_timeline messages.
32+func (h *HistoryHandlers) HandleTimeline() webkit.MessageHandler {
33+ return webkit.MessageHandlerFunc(func(ctx context.Context, webviewID webkit.WebViewID, payload json.RawMessage) (any, error) {
34+ log := logging.FromContext(ctx)
35+
36+ var req timelineRequest
37+ if err := json.Unmarshal(payload, &req); err != nil {
38+ return NewErrorResponse("", err), nil
39+ }
40+
41+ log.Debug().
42+ Str("request_id", req.RequestID).
43+ Int("limit", req.Limit).
44+ Int("offset", req.Offset).
45+ Msg("handling history_timeline")
46+
47+ entries, err := h.historyUC.GetRecent(ctx, req.Limit, req.Offset)
48+ if err != nil {
49+ return NewErrorResponse(req.RequestID, err), nil
50+ }
51+
52+ return NewSuccessResponse(req.RequestID, entries), nil
53+ })
54+}
55+
56+// searchRequest is the payload for history_search_fts messages.
57+type searchRequest struct {
58+ RequestID string `json:"requestId"`
59+ Query string `json:"query"`
60+ Limit int `json:"limit"`
61+}
62+
63+// HandleSearchFTS handles history_search_fts messages.
64+func (h *HistoryHandlers) HandleSearchFTS() webkit.MessageHandler {
65+ return webkit.MessageHandlerFunc(func(ctx context.Context, webviewID webkit.WebViewID, payload json.RawMessage) (any, error) {
66+ log := logging.FromContext(ctx)
67+
68+ var req searchRequest
69+ if err := json.Unmarshal(payload, &req); err != nil {
70+ return NewErrorResponse("", err), nil
71+ }
72+
73+ log.Debug().
74+ Str("request_id", req.RequestID).
75+ Str("query", req.Query).
76+ Msg("handling history_search_fts")
77+
78+ output, err := h.historyUC.Search(ctx, usecase.SearchInput{
79+ Query: req.Query,
80+ Limit: req.Limit,
81+ })
82+ if err != nil {
83+ return NewErrorResponse(req.RequestID, err), nil
84+ }
85+
86+ // Convert matches to entries for frontend compatibility
87+ entries := make([]*entity.HistoryEntry, 0, len(output.Matches))
88+ for _, m := range output.Matches {
89+ entries = append(entries, m.Entry)
90+ }
91+
92+ return NewSuccessResponse(req.RequestID, entries), nil
93+ })
94+}
95+
96+// deleteEntryRequest is the payload for history_delete_entry messages.
97+type deleteEntryRequest struct {
98+ RequestID string `json:"requestId"`
99+ ID int64 `json:"id"`
100+}
101+
102+// HandleDeleteEntry handles history_delete_entry messages.
103+func (h *HistoryHandlers) HandleDeleteEntry() webkit.MessageHandler {
104+ return webkit.MessageHandlerFunc(func(ctx context.Context, webviewID webkit.WebViewID, payload json.RawMessage) (any, error) {
105+ log := logging.FromContext(ctx)
106+
107+ var req deleteEntryRequest
108+ if err := json.Unmarshal(payload, &req); err != nil {
109+ return NewErrorResponse("", err), nil
110+ }
111+
112+ log.Debug().
113+ Str("request_id", req.RequestID).
114+ Int64("id", req.ID).
115+ Msg("handling history_delete_entry")
116+
117+ if err := h.historyUC.Delete(ctx, req.ID); err != nil {
118+ return NewErrorResponse(req.RequestID, err), nil
119+ }
120+
121+ return NewSuccessResponse(req.RequestID, nil), nil
122+ })
123+}
124+
125+// deleteRangeRequest is the payload for history_delete_range messages.
126+type deleteRangeRequest struct {
127+ RequestID string `json:"requestId"`
128+ Range string `json:"range"` // "hour", "day", "week", "month", "all"
129+}
130+
131+// HandleDeleteRange handles history_delete_range messages.
132+func (h *HistoryHandlers) HandleDeleteRange() webkit.MessageHandler {
133+ return webkit.MessageHandlerFunc(func(ctx context.Context, webviewID webkit.WebViewID, payload json.RawMessage) (any, error) {
134+ log := logging.FromContext(ctx)
135+
136+ var req deleteRangeRequest
137+ if err := json.Unmarshal(payload, &req); err != nil {
138+ return NewErrorResponse("", err), nil
139+ }
140+
141+ log.Debug().
142+ Str("request_id", req.RequestID).
143+ Str("range", req.Range).
144+ Msg("handling history_delete_range")
145+
146+ before := rangeToTime(req.Range)
147+ if before.IsZero() {
148+ // "all" case - clear all
149+ if err := h.historyUC.ClearAll(ctx); err != nil {
150+ return NewErrorResponse(req.RequestID, err), nil
151+ }
152+ } else {
153+ if err := h.historyUC.ClearOlderThan(ctx, before); err != nil {
154+ return NewErrorResponse(req.RequestID, err), nil
155+ }
156+ }
157+
158+ return NewSuccessResponse(req.RequestID, nil), nil
159+ })
160+}
161+
162+// HandleClearAll handles history_clear_all messages.
163+func (h *HistoryHandlers) HandleClearAll() webkit.MessageHandler {
164+ return webkit.MessageHandlerFunc(func(ctx context.Context, webviewID webkit.WebViewID, payload json.RawMessage) (any, error) {
165+ log := logging.FromContext(ctx)
166+
167+ requestID := ParseRequestID(payload)
168+
169+ log.Debug().
170+ Str("request_id", requestID).
171+ Msg("handling history_clear_all")
172+
173+ if err := h.historyUC.ClearAll(ctx); err != nil {
174+ return NewErrorResponse(requestID, err), nil
175+ }
176+
177+ return NewSuccessResponse(requestID, nil), nil
178+ })
179+}
180+
181+// rangeToTime converts a range string to a time.Time cutoff.
182+// Returns zero time for "all" which means delete everything.
183+func rangeToTime(r string) time.Time {
184+ now := time.Now()
185+ switch r {
186+ case "hour":
187+ return now.Add(-time.Hour)
188+ case "day":
189+ return now.AddDate(0, 0, -1)
190+ case "week":
191+ return now.AddDate(0, 0, -7)
192+ case "month":
193+ return now.AddDate(0, -1, 0)
194+ case "all":
195+ return time.Time{} // Zero time signals "delete all"
196+ default:
197+ return now
198+ }
199+}
200+
201+// HandleAnalytics handles history_analytics messages.
202+func (h *HistoryHandlers) HandleAnalytics() webkit.MessageHandler {
203+ return webkit.MessageHandlerFunc(func(ctx context.Context, webviewID webkit.WebViewID, payload json.RawMessage) (any, error) {
204+ log := logging.FromContext(ctx)
205+
206+ requestID := ParseRequestID(payload)
207+
208+ log.Debug().
209+ Str("request_id", requestID).
210+ Msg("handling history_analytics")
211+
212+ analytics, err := h.historyUC.GetAnalytics(ctx)
213+ if err != nil {
214+ return NewErrorResponse(requestID, err), nil
215+ }
216+
217+ return NewSuccessResponse(requestID, analytics), nil
218+ })
219+}
220+
221+// domainStatsRequest is the payload for history_domain_stats messages.
222+type domainStatsRequest struct {
223+ RequestID string `json:"requestId"`
224+ Limit int `json:"limit"`
225+}
226+
227+// HandleDomainStats handles history_domain_stats messages.
228+func (h *HistoryHandlers) HandleDomainStats() webkit.MessageHandler {
229+ return webkit.MessageHandlerFunc(func(ctx context.Context, webviewID webkit.WebViewID, payload json.RawMessage) (any, error) {
230+ log := logging.FromContext(ctx)
231+
232+ var req domainStatsRequest
233+ if err := json.Unmarshal(payload, &req); err != nil {
234+ return NewErrorResponse("", err), nil
235+ }
236+
237+ log.Debug().
238+ Str("request_id", req.RequestID).
239+ Int("limit", req.Limit).
240+ Msg("handling history_domain_stats")
241+
242+ stats, err := h.historyUC.GetDomainStats(ctx, req.Limit)
243+ if err != nil {
244+ return NewErrorResponse(req.RequestID, err), nil
245+ }
246+
247+ return NewSuccessResponse(req.RequestID, stats), nil
248+ })
249+}
250+
251+// deleteDomainRequest is the payload for history_delete_domain messages.
252+type deleteDomainRequest struct {
253+ RequestID string `json:"requestId"`
254+ Domain string `json:"domain"`
255+}
256+
257+// HandleDeleteDomain handles history_delete_domain messages.
258+func (h *HistoryHandlers) HandleDeleteDomain() webkit.MessageHandler {
259+ return webkit.MessageHandlerFunc(func(ctx context.Context, webviewID webkit.WebViewID, payload json.RawMessage) (any, error) {
260+ log := logging.FromContext(ctx)
261+
262+ var req deleteDomainRequest
263+ if err := json.Unmarshal(payload, &req); err != nil {
264+ return NewErrorResponse("", err), nil
265+ }
266+
267+ log.Debug().
268+ Str("request_id", req.RequestID).
269+ Str("domain", req.Domain).
270+ Msg("handling history_delete_domain")
271+
272+ if err := h.historyUC.DeleteByDomain(ctx, req.Domain); err != nil {
273+ return NewErrorResponse(req.RequestID, err), nil
274+ }
275+
276+ return NewSuccessResponse(req.RequestID, nil), nil
277+ })
278+}
@@ -0,0 +1,278 @@
1+package homepage
2+
3+import (
4+ "context"
5+ "encoding/json"
6+ "time"
7+
8+ "github.com/bnema/dumber/internal/application/usecase"
9+ "github.com/bnema/dumber/internal/domain/entity"
10+ "github.com/bnema/dumber/internal/infrastructure/webkit"
11+ "github.com/bnema/dumber/internal/logging"
12+)
13+
14+// HistoryHandlers handles history-related messages from the homepage.
15+type HistoryHandlers struct {
16+ historyUC *usecase.SearchHistoryUseCase
17+}
18+
19+// NewHistoryHandlers creates a new HistoryHandlers instance.
20+func NewHistoryHandlers(historyUC *usecase.SearchHistoryUseCase) *HistoryHandlers {
21+ return &HistoryHandlers{historyUC: historyUC}
22+}
23+
24+// timelineRequest is the payload for history_timeline messages.
25+type timelineRequest struct {
26+ RequestID string `json:"requestId"`
27+ Limit int `json:"limit"`
28+ Offset int `json:"offset"`
29+}
30+
31+// HandleTimeline handles history_timeline messages.
32+func (h *HistoryHandlers) HandleTimeline() webkit.MessageHandler {
33+ return webkit.MessageHandlerFunc(func(ctx context.Context, webviewID webkit.WebViewID, payload json.RawMessage) (any, error) {
34+ log := logging.FromContext(ctx)
35+
36+ var req timelineRequest
37+ if err := json.Unmarshal(payload, &req); err != nil {
38+ return NewErrorResponse("", err), nil
39+ }
40+
41+ log.Debug().
42+ Str("request_id", req.RequestID).
43+ Int("limit", req.Limit).
44+ Int("offset", req.Offset).
45+ Msg("handling history_timeline")
46+
47+ entries, err := h.historyUC.GetRecent(ctx, req.Limit, req.Offset)
48+ if err != nil {
49+ return NewErrorResponse(req.RequestID, err), nil
50+ }
51+
52+ return NewSuccessResponse(req.RequestID, entries), nil
53+ })
54+}
55+
56+// searchRequest is the payload for history_search_fts messages.
57+type searchRequest struct {
58+ RequestID string `json:"requestId"`
59+ Query string `json:"query"`
60+ Limit int `json:"limit"`
61+}
62+
63+// HandleSearchFTS handles history_search_fts messages.
64+func (h *HistoryHandlers) HandleSearchFTS() webkit.MessageHandler {
65+ return webkit.MessageHandlerFunc(func(ctx context.Context, webviewID webkit.WebViewID, payload json.RawMessage) (any, error) {
66+ log := logging.FromContext(ctx)
67+
68+ var req searchRequest
69+ if err := json.Unmarshal(payload, &req); err != nil {
70+ return NewErrorResponse("", err), nil
71+ }
72+
73+ log.Debug().
74+ Str("request_id", req.RequestID).
75+ Str("query", req.Query).
76+ Msg("handling history_search_fts")
77+
78+ output, err := h.historyUC.Search(ctx, usecase.SearchInput{
79+ Query: req.Query,
80+ Limit: req.Limit,
81+ })
82+ if err != nil {
83+ return NewErrorResponse(req.RequestID, err), nil
84+ }
85+
86+ // Convert matches to entries for frontend compatibility
87+ entries := make([]*entity.HistoryEntry, 0, len(output.Matches))
88+ for _, m := range output.Matches {
89+ entries = append(entries, m.Entry)
90+ }
91+
92+ return NewSuccessResponse(req.RequestID, entries), nil
93+ })
94+}
95+
96+// deleteEntryRequest is the payload for history_delete_entry messages.
97+type deleteEntryRequest struct {
98+ RequestID string `json:"requestId"`
99+ ID int64 `json:"id"`
100+}
101+
102+// HandleDeleteEntry handles history_delete_entry messages.
103+func (h *HistoryHandlers) HandleDeleteEntry() webkit.MessageHandler {
104+ return webkit.MessageHandlerFunc(func(ctx context.Context, webviewID webkit.WebViewID, payload json.RawMessage) (any, error) {
105+ log := logging.FromContext(ctx)
106+
107+ var req deleteEntryRequest
108+ if err := json.Unmarshal(payload, &req); err != nil {
109+ return NewErrorResponse("", err), nil
110+ }
111+
112+ log.Debug().
113+ Str("request_id", req.RequestID).
114+ Int64("id", req.ID).
115+ Msg("handling history_delete_entry")
116+
117+ if err := h.historyUC.Delete(ctx, req.ID); err != nil {
118+ return NewErrorResponse(req.RequestID, err), nil
119+ }
120+
121+ return NewSuccessResponse(req.RequestID, nil), nil
122+ })
123+}
124+
125+// deleteRangeRequest is the payload for history_delete_range messages.
126+type deleteRangeRequest struct {
127+ RequestID string `json:"requestId"`
128+ Range string `json:"range"` // "hour", "day", "week", "month", "all"
129+}
130+
131+// HandleDeleteRange handles history_delete_range messages.
132+func (h *HistoryHandlers) HandleDeleteRange() webkit.MessageHandler {
133+ return webkit.MessageHandlerFunc(func(ctx context.Context, webviewID webkit.WebViewID, payload json.RawMessage) (any, error) {
134+ log := logging.FromContext(ctx)
135+
136+ var req deleteRangeRequest
137+ if err := json.Unmarshal(payload, &req); err != nil {
138+ return NewErrorResponse("", err), nil
139+ }
140+
141+ log.Debug().
142+ Str("request_id", req.RequestID).
143+ Str("range", req.Range).
144+ Msg("handling history_delete_range")
145+
146+ before := rangeToTime(req.Range)
147+ if before.IsZero() {
148+ // "all" case - clear all
149+ if err := h.historyUC.ClearAll(ctx); err != nil {
150+ return NewErrorResponse(req.RequestID, err), nil
151+ }
152+ } else {
153+ if err := h.historyUC.ClearOlderThan(ctx, before); err != nil {
154+ return NewErrorResponse(req.RequestID, err), nil
155+ }
156+ }
157+
158+ return NewSuccessResponse(req.RequestID, nil), nil
159+ })
160+}
161+
162+// HandleClearAll handles history_clear_all messages.
163+func (h *HistoryHandlers) HandleClearAll() webkit.MessageHandler {
164+ return webkit.MessageHandlerFunc(func(ctx context.Context, webviewID webkit.WebViewID, payload json.RawMessage) (any, error) {
165+ log := logging.FromContext(ctx)
166+
167+ requestID := ParseRequestID(payload)
168+
169+ log.Debug().
170+ Str("request_id", requestID).
171+ Msg("handling history_clear_all")
172+
173+ if err := h.historyUC.ClearAll(ctx); err != nil {
174+ return NewErrorResponse(requestID, err), nil
175+ }
176+
177+ return NewSuccessResponse(requestID, nil), nil
178+ })
179+}
180+
181+// rangeToTime converts a range string to a time.Time cutoff.
182+// Returns zero time for "all" which means delete everything.
183+func rangeToTime(r string) time.Time {
184+ now := time.Now()
185+ switch r {
186+ case "hour":
187+ return now.Add(-time.Hour)
188+ case "day":
189+ return now.AddDate(0, 0, -1)
190+ case "week":
191+ return now.AddDate(0, 0, -7)
192+ case "month":
193+ return now.AddDate(0, -1, 0)
194+ case "all":
195+ return time.Time{} // Zero time signals "delete all"
196+ default:
197+ return now
198+ }
199+}
200+
201+// HandleAnalytics handles history_analytics messages.
202+func (h *HistoryHandlers) HandleAnalytics() webkit.MessageHandler {
203+ return webkit.MessageHandlerFunc(func(ctx context.Context, webviewID webkit.WebViewID, payload json.RawMessage) (any, error) {
204+ log := logging.FromContext(ctx)
205+
206+ requestID := ParseRequestID(payload)
207+
208+ log.Debug().
209+ Str("request_id", requestID).
210+ Msg("handling history_analytics")
211+
212+ analytics, err := h.historyUC.GetAnalytics(ctx)
213+ if err != nil {
214+ return NewErrorResponse(requestID, err), nil
215+ }
216+
217+ return NewSuccessResponse(requestID, analytics), nil
218+ })
219+}
220+
221+// domainStatsRequest is the payload for history_domain_stats messages.
222+type domainStatsRequest struct {
223+ RequestID string `json:"requestId"`
224+ Limit int `json:"limit"`
225+}
226+
227+// HandleDomainStats handles history_domain_stats messages.
228+func (h *HistoryHandlers) HandleDomainStats() webkit.MessageHandler {
229+ return webkit.MessageHandlerFunc(func(ctx context.Context, webviewID webkit.WebViewID, payload json.RawMessage) (any, error) {
230+ log := logging.FromContext(ctx)
231+
232+ var req domainStatsRequest
233+ if err := json.Unmarshal(payload, &req); err != nil {
234+ return NewErrorResponse("", err), nil
235+ }
236+
237+ log.Debug().
238+ Str("request_id", req.RequestID).
239+ Int("limit", req.Limit).
240+ Msg("handling history_domain_stats")
241+
242+ stats, err := h.historyUC.GetDomainStats(ctx, req.Limit)
243+ if err != nil {
244+ return NewErrorResponse(req.RequestID, err), nil
245+ }
246+
247+ return NewSuccessResponse(req.RequestID, stats), nil
248+ })
249+}
250+
251+// deleteDomainRequest is the payload for history_delete_domain messages.
252+type deleteDomainRequest struct {
253+ RequestID string `json:"requestId"`
254+ Domain string `json:"domain"`
255+}
256+
257+// HandleDeleteDomain handles history_delete_domain messages.
258+func (h *HistoryHandlers) HandleDeleteDomain() webkit.MessageHandler {
259+ return webkit.MessageHandlerFunc(func(ctx context.Context, webviewID webkit.WebViewID, payload json.RawMessage) (any, error) {
260+ log := logging.FromContext(ctx)
261+
262+ var req deleteDomainRequest
263+ if err := json.Unmarshal(payload, &req); err != nil {
264+ return NewErrorResponse("", err), nil
265+ }
266+
267+ log.Debug().
268+ Str("request_id", req.RequestID).
269+ Str("domain", req.Domain).
270+ Msg("handling history_delete_domain")
271+
272+ if err := h.historyUC.DeleteByDomain(ctx, req.Domain); err != nil {
273+ return NewErrorResponse(req.RequestID, err), nil
274+ }
275+
276+ return NewSuccessResponse(req.RequestID, nil), nil
277+ })
278+}
A internal/infrastructure/webkit/handlers/homepage/registry.go +74 -0
@@ -0,0 +1,74 @@
1+package homepage
2+
3+import (
4+ "context"
5+ "fmt"
6+
7+ "github.com/bnema/dumber/internal/application/usecase"
8+ "github.com/bnema/dumber/internal/infrastructure/webkit"
9+ "github.com/bnema/dumber/internal/logging"
10+)
11+
12+// Config holds dependencies for homepage handlers.
13+type Config struct {
14+ HistoryUC *usecase.SearchHistoryUseCase
15+ FavoritesUC *usecase.ManageFavoritesUseCase
16+}
17+
18+// RegisterHandlers registers all homepage message handlers with the router.
19+func RegisterHandlers(ctx context.Context, router *webkit.MessageRouter, cfg Config) error {
20+ log := logging.FromContext(ctx)
21+ log.Debug().Msg("registering homepage message handlers")
22+
23+ // Common callback for all homepage handlers
24+ const callback = "__dumber_homepage_response"
25+ const errorCallback = "__dumber_error"
26+ const worldName = "" // main world
27+
28+ handlers := make(map[string]webkit.MessageHandler)
29+
30+ // History handlers
31+ historyHandlers := NewHistoryHandlers(cfg.HistoryUC)
32+ handlers["history_timeline"] = historyHandlers.HandleTimeline()
33+ handlers["history_search_fts"] = historyHandlers.HandleSearchFTS()
34+ handlers["history_delete_entry"] = historyHandlers.HandleDeleteEntry()
35+ handlers["history_delete_range"] = historyHandlers.HandleDeleteRange()
36+ handlers["history_clear_all"] = historyHandlers.HandleClearAll()
37+ handlers["history_analytics"] = historyHandlers.HandleAnalytics()
38+ handlers["history_domain_stats"] = historyHandlers.HandleDomainStats()
39+ handlers["history_delete_domain"] = historyHandlers.HandleDeleteDomain()
40+
41+ // Favorites handlers
42+ favoritesHandlers := NewFavoritesHandlers(cfg.FavoritesUC)
43+ handlers["favorite_list"] = favoritesHandlers.HandleList()
44+ handlers["favorite_set_shortcut"] = favoritesHandlers.HandleSetShortcut()
45+ handlers["favorite_get_by_shortcut"] = favoritesHandlers.HandleGetByShortcut()
46+ handlers["favorite_set_folder"] = favoritesHandlers.HandleSetFolder()
47+
48+ // Folder handlers
49+ folderHandlers := NewFolderHandlers(cfg.FavoritesUC)
50+ handlers["folder_list"] = folderHandlers.HandleList()
51+ handlers["folder_create"] = folderHandlers.HandleCreate()
52+ handlers["folder_delete"] = folderHandlers.HandleDelete()
53+ handlers["folder_update"] = folderHandlers.HandleUpdate() // Requires UpdateFolder()
54+
55+ // Tag handlers
56+ tagHandlers := NewTagHandlers(cfg.FavoritesUC)
57+ handlers["tag_list"] = tagHandlers.HandleList()
58+ handlers["tag_create"] = tagHandlers.HandleCreate()
59+ handlers["tag_delete"] = tagHandlers.HandleDelete()
60+ handlers["tag_update"] = tagHandlers.HandleUpdate() // Requires UpdateTag()
61+ handlers["tag_assign"] = tagHandlers.HandleAssign()
62+ handlers["tag_remove"] = tagHandlers.HandleRemove()
63+
64+ // Register all handlers
65+ for msgType, handler := range handlers {
66+ if err := router.RegisterHandlerWithCallbacks(msgType, callback, errorCallback, worldName, handler); err != nil {
67+ return fmt.Errorf("failed to register handler %s: %w", msgType, err)
68+ }
69+ log.Debug().Str("type", msgType).Msg("registered homepage handler")
70+ }
71+
72+ log.Info().Int("count", len(handlers)).Msg("homepage handlers registered")
73+ return nil
74+}
@@ -0,0 +1,74 @@
1+package homepage
2+
3+import (
4+ "context"
5+ "fmt"
6+
7+ "github.com/bnema/dumber/internal/application/usecase"
8+ "github.com/bnema/dumber/internal/infrastructure/webkit"
9+ "github.com/bnema/dumber/internal/logging"
10+)
11+
12+// Config holds dependencies for homepage handlers.
13+type Config struct {
14+ HistoryUC *usecase.SearchHistoryUseCase
15+ FavoritesUC *usecase.ManageFavoritesUseCase
16+}
17+
18+// RegisterHandlers registers all homepage message handlers with the router.
19+func RegisterHandlers(ctx context.Context, router *webkit.MessageRouter, cfg Config) error {
20+ log := logging.FromContext(ctx)
21+ log.Debug().Msg("registering homepage message handlers")
22+
23+ // Common callback for all homepage handlers
24+ const callback = "__dumber_homepage_response"
25+ const errorCallback = "__dumber_error"
26+ const worldName = "" // main world
27+
28+ handlers := make(map[string]webkit.MessageHandler)
29+
30+ // History handlers
31+ historyHandlers := NewHistoryHandlers(cfg.HistoryUC)
32+ handlers["history_timeline"] = historyHandlers.HandleTimeline()
33+ handlers["history_search_fts"] = historyHandlers.HandleSearchFTS()
34+ handlers["history_delete_entry"] = historyHandlers.HandleDeleteEntry()
35+ handlers["history_delete_range"] = historyHandlers.HandleDeleteRange()
36+ handlers["history_clear_all"] = historyHandlers.HandleClearAll()
37+ handlers["history_analytics"] = historyHandlers.HandleAnalytics()
38+ handlers["history_domain_stats"] = historyHandlers.HandleDomainStats()
39+ handlers["history_delete_domain"] = historyHandlers.HandleDeleteDomain()
40+
41+ // Favorites handlers
42+ favoritesHandlers := NewFavoritesHandlers(cfg.FavoritesUC)
43+ handlers["favorite_list"] = favoritesHandlers.HandleList()
44+ handlers["favorite_set_shortcut"] = favoritesHandlers.HandleSetShortcut()
45+ handlers["favorite_get_by_shortcut"] = favoritesHandlers.HandleGetByShortcut()
46+ handlers["favorite_set_folder"] = favoritesHandlers.HandleSetFolder()
47+
48+ // Folder handlers
49+ folderHandlers := NewFolderHandlers(cfg.FavoritesUC)
50+ handlers["folder_list"] = folderHandlers.HandleList()
51+ handlers["folder_create"] = folderHandlers.HandleCreate()
52+ handlers["folder_delete"] = folderHandlers.HandleDelete()
53+ handlers["folder_update"] = folderHandlers.HandleUpdate() // Requires UpdateFolder()
54+
55+ // Tag handlers
56+ tagHandlers := NewTagHandlers(cfg.FavoritesUC)
57+ handlers["tag_list"] = tagHandlers.HandleList()
58+ handlers["tag_create"] = tagHandlers.HandleCreate()
59+ handlers["tag_delete"] = tagHandlers.HandleDelete()
60+ handlers["tag_update"] = tagHandlers.HandleUpdate() // Requires UpdateTag()
61+ handlers["tag_assign"] = tagHandlers.HandleAssign()
62+ handlers["tag_remove"] = tagHandlers.HandleRemove()
63+
64+ // Register all handlers
65+ for msgType, handler := range handlers {
66+ if err := router.RegisterHandlerWithCallbacks(msgType, callback, errorCallback, worldName, handler); err != nil {
67+ return fmt.Errorf("failed to register handler %s: %w", msgType, err)
68+ }
69+ log.Debug().Str("type", msgType).Msg("registered homepage handler")
70+ }
71+
72+ log.Info().Int("count", len(handlers)).Msg("homepage handlers registered")
73+ return nil
74+}
A internal/infrastructure/webkit/handlers/homepage/response.go +65 -0
@@ -0,0 +1,65 @@
1+package homepage
2+
3+import (
4+ "encoding/json"
5+)
6+
7+// Response represents the standard response format sent back to the frontend.
8+// The frontend expects: { requestId, success, data?, error? }
9+type Response struct {
10+ RequestID string `json:"requestId"`
11+ Success bool `json:"success"`
12+ Data any `json:"data,omitempty"`
13+ Error string `json:"error,omitempty"`
14+}
15+
16+// NewSuccessResponse creates a success response with data.
17+func NewSuccessResponse(requestID string, data any) Response {
18+ return Response{
19+ RequestID: requestID,
20+ Success: true,
21+ Data: data,
22+ }
23+}
24+
25+// NewErrorResponse creates an error response.
26+func NewErrorResponse(requestID string, err error) Response {
27+ errMsg := "unknown error"
28+ if err != nil {
29+ errMsg = err.Error()
30+ }
31+ return Response{
32+ RequestID: requestID,
33+ Success: false,
34+ Error: errMsg,
35+ }
36+}
37+
38+// baseRequest is used to extract the requestId from any payload.
39+type baseRequest struct {
40+ RequestID string `json:"requestId"`
41+}
42+
43+// ParseRequestID extracts the requestId from a raw JSON payload.
44+func ParseRequestID(payload json.RawMessage) string {
45+ var base baseRequest
46+ if err := json.Unmarshal(payload, &base); err != nil {
47+ return ""
48+ }
49+ return base.RequestID
50+}
51+
52+// ParsePayload extracts the requestId and unmarshals the payload into the target type.
53+// Returns the requestId, the parsed payload, and any error.
54+func ParsePayload[T any](payload json.RawMessage) (string, T, error) {
55+ var base baseRequest
56+ var target T
57+
58+ if err := json.Unmarshal(payload, &base); err != nil {
59+ return "", target, err
60+ }
61+ if err := json.Unmarshal(payload, &target); err != nil {
62+ return base.RequestID, target, err
63+ }
64+ return base.RequestID, target, nil
65+}
@@ -0,0 +1,65 @@
1+package homepage
2+
3+import (
4+ "encoding/json"
5+)
6+
7+// Response represents the standard response format sent back to the frontend.
8+// The frontend expects: { requestId, success, data?, error? }
9+type Response struct {
10+ RequestID string `json:"requestId"`
11+ Success bool `json:"success"`
12+ Data any `json:"data,omitempty"`
13+ Error string `json:"error,omitempty"`
14+}
15+
16+// NewSuccessResponse creates a success response with data.
17+func NewSuccessResponse(requestID string, data any) Response {
18+ return Response{
19+ RequestID: requestID,
20+ Success: true,
21+ Data: data,
22+ }
23+}
24+
25+// NewErrorResponse creates an error response.
26+func NewErrorResponse(requestID string, err error) Response {
27+ errMsg := "unknown error"
28+ if err != nil {
29+ errMsg = err.Error()
30+ }
31+ return Response{
32+ RequestID: requestID,
33+ Success: false,
34+ Error: errMsg,
35+ }
36+}
37+
38+// baseRequest is used to extract the requestId from any payload.
39+type baseRequest struct {
40+ RequestID string `json:"requestId"`
41+}
42+
43+// ParseRequestID extracts the requestId from a raw JSON payload.
44+func ParseRequestID(payload json.RawMessage) string {
45+ var base baseRequest
46+ if err := json.Unmarshal(payload, &base); err != nil {
47+ return ""
48+ }
49+ return base.RequestID
50+}
51+
52+// ParsePayload extracts the requestId and unmarshals the payload into the target type.
53+// Returns the requestId, the parsed payload, and any error.
54+func ParsePayload[T any](payload json.RawMessage) (string, T, error) {
55+ var base baseRequest
56+ var target T
57+
58+ if err := json.Unmarshal(payload, &base); err != nil {
59+ return "", target, err
60+ }
61+ if err := json.Unmarshal(payload, &target); err != nil {
62+ return base.RequestID, target, err
63+ }
64+ return base.RequestID, target, nil
65+}
A internal/infrastructure/webkit/handlers/homepage/tags.go +202 -0
@@ -0,0 +1,202 @@
1+package homepage
2+
3+import (
4+ "context"
5+ "encoding/json"
6+
7+ "github.com/bnema/dumber/internal/application/usecase"
8+ "github.com/bnema/dumber/internal/domain/entity"
9+ "github.com/bnema/dumber/internal/infrastructure/webkit"
10+ "github.com/bnema/dumber/internal/logging"
11+)
12+
13+// TagHandlers handles tag-related messages from the homepage.
14+type TagHandlers struct {
15+ favoritesUC *usecase.ManageFavoritesUseCase
16+}
17+
18+// NewTagHandlers creates a new TagHandlers instance.
19+func NewTagHandlers(favoritesUC *usecase.ManageFavoritesUseCase) *TagHandlers {
20+ return &TagHandlers{favoritesUC: favoritesUC}
21+}
22+
23+// HandleList handles tag_list messages.
24+func (h *TagHandlers) HandleList() webkit.MessageHandler {
25+ return webkit.MessageHandlerFunc(func(ctx context.Context, webviewID webkit.WebViewID, payload json.RawMessage) (any, error) {
26+ log := logging.FromContext(ctx)
27+
28+ requestID := ParseRequestID(payload)
29+
30+ log.Debug().
31+ Str("request_id", requestID).
32+ Msg("handling tag_list")
33+
34+ tags, err := h.favoritesUC.GetAllTags(ctx)
35+ if err != nil {
36+ return NewErrorResponse(requestID, err), nil
37+ }
38+
39+ return NewSuccessResponse(requestID, tags), nil
40+ })
41+}
42+
43+// createTagRequest is the payload for tag_create messages.
44+type createTagRequest struct {
45+ RequestID string `json:"requestId"`
46+ Name string `json:"name"`
47+ Color *string `json:"color"`
48+}
49+
50+// HandleCreate handles tag_create messages.
51+func (h *TagHandlers) HandleCreate() webkit.MessageHandler {
52+ return webkit.MessageHandlerFunc(func(ctx context.Context, webviewID webkit.WebViewID, payload json.RawMessage) (any, error) {
53+ log := logging.FromContext(ctx)
54+
55+ var req createTagRequest
56+ if err := json.Unmarshal(payload, &req); err != nil {
57+ return NewErrorResponse("", err), nil
58+ }
59+
60+ log.Debug().
61+ Str("request_id", req.RequestID).
62+ Str("name", req.Name).
63+ Msg("handling tag_create")
64+
65+ color := ""
66+ if req.Color != nil {
67+ color = *req.Color
68+ }
69+
70+ tag, err := h.favoritesUC.AddTag(ctx, req.Name, color)
71+ if err != nil {
72+ return NewErrorResponse(req.RequestID, err), nil
73+ }
74+
75+ return NewSuccessResponse(req.RequestID, tag), nil
76+ })
77+}
78+
79+// deleteTagRequest is the payload for tag_delete messages.
80+type deleteTagRequest struct {
81+ RequestID string `json:"requestId"`
82+ ID int64 `json:"id"`
83+}
84+
85+// HandleDelete handles tag_delete messages.
86+func (h *TagHandlers) HandleDelete() webkit.MessageHandler {
87+ return webkit.MessageHandlerFunc(func(ctx context.Context, webviewID webkit.WebViewID, payload json.RawMessage) (any, error) {
88+ log := logging.FromContext(ctx)
89+
90+ var req deleteTagRequest
91+ if err := json.Unmarshal(payload, &req); err != nil {
92+ return NewErrorResponse("", err), nil
93+ }
94+
95+ log.Debug().
96+ Str("request_id", req.RequestID).
97+ Int64("id", req.ID).
98+ Msg("handling tag_delete")
99+
100+ if err := h.favoritesUC.DeleteTag(ctx, entity.TagID(req.ID)); err != nil {
101+ return NewErrorResponse(req.RequestID, err), nil
102+ }
103+
104+ return NewSuccessResponse(req.RequestID, nil), nil
105+ })
106+}
107+
108+// updateTagRequest is the payload for tag_update messages.
109+type updateTagRequest struct {
110+ RequestID string `json:"requestId"`
111+ ID int64 `json:"id"`
112+ Name *string `json:"name"`
113+ Color *string `json:"color"`
114+}
115+
116+// HandleUpdate handles tag_update messages.
117+// NOTE: This requires UpdateTag() method to be added to ManageFavoritesUseCase.
118+func (h *TagHandlers) HandleUpdate() webkit.MessageHandler {
119+ return webkit.MessageHandlerFunc(func(ctx context.Context, webviewID webkit.WebViewID, payload json.RawMessage) (any, error) {
120+ log := logging.FromContext(ctx)
121+
122+ var req updateTagRequest
123+ if err := json.Unmarshal(payload, &req); err != nil {
124+ return NewErrorResponse("", err), nil
125+ }
126+
127+ log.Debug().
128+ Str("request_id", req.RequestID).
129+ Int64("id", req.ID).
130+ Msg("handling tag_update")
131+
132+ name := ""
133+ if req.Name != nil {
134+ name = *req.Name
135+ }
136+ color := ""
137+ if req.Color != nil {
138+ color = *req.Color
139+ }
140+
141+ if err := h.favoritesUC.UpdateTag(ctx, entity.TagID(req.ID), name, color); err != nil {
142+ return NewErrorResponse(req.RequestID, err), nil
143+ }
144+
145+ return NewSuccessResponse(req.RequestID, nil), nil
146+ })
147+}
148+
149+// tagAssignRequest is the payload for tag_assign messages.
150+type tagAssignRequest struct {
151+ RequestID string `json:"requestId"`
152+ FavoriteID int64 `json:"favorite_id"`
153+ TagID int64 `json:"tag_id"`
154+}
155+
156+// HandleAssign handles tag_assign messages.
157+func (h *TagHandlers) HandleAssign() webkit.MessageHandler {
158+ return webkit.MessageHandlerFunc(func(ctx context.Context, webviewID webkit.WebViewID, payload json.RawMessage) (any, error) {
159+ log := logging.FromContext(ctx)
160+
161+ var req tagAssignRequest
162+ if err := json.Unmarshal(payload, &req); err != nil {
163+ return NewErrorResponse("", err), nil
164+ }
165+
166+ log.Debug().
167+ Str("request_id", req.RequestID).
168+ Int64("favorite_id", req.FavoriteID).
169+ Int64("tag_id", req.TagID).
170+ Msg("handling tag_assign")
171+
172+ if err := h.favoritesUC.TagFavorite(ctx, entity.FavoriteID(req.FavoriteID), entity.TagID(req.TagID)); err != nil {
173+ return NewErrorResponse(req.RequestID, err), nil
174+ }
175+
176+ return NewSuccessResponse(req.RequestID, nil), nil
177+ })
178+}
179+
180+// HandleRemove handles tag_remove messages.
181+func (h *TagHandlers) HandleRemove() webkit.MessageHandler {
182+ return webkit.MessageHandlerFunc(func(ctx context.Context, webviewID webkit.WebViewID, payload json.RawMessage) (any, error) {
183+ log := logging.FromContext(ctx)
184+
185+ var req tagAssignRequest // same structure as assign
186+ if err := json.Unmarshal(payload, &req); err != nil {
187+ return NewErrorResponse("", err), nil
188+ }
189+
190+ log.Debug().
191+ Str("request_id", req.RequestID).
192+ Int64("favorite_id", req.FavoriteID).
193+ Int64("tag_id", req.TagID).
194+ Msg("handling tag_remove")
195+
196+ if err := h.favoritesUC.UntagFavorite(ctx, entity.FavoriteID(req.FavoriteID), entity.TagID(req.TagID)); err != nil {
197+ return NewErrorResponse(req.RequestID, err), nil
198+ }
199+
200+ return NewSuccessResponse(req.RequestID, nil), nil
201+ })
202+}
@@ -0,0 +1,202 @@
1+package homepage
2+
3+import (
4+ "context"
5+ "encoding/json"
6+
7+ "github.com/bnema/dumber/internal/application/usecase"
8+ "github.com/bnema/dumber/internal/domain/entity"
9+ "github.com/bnema/dumber/internal/infrastructure/webkit"
10+ "github.com/bnema/dumber/internal/logging"
11+)
12+
13+// TagHandlers handles tag-related messages from the homepage.
14+type TagHandlers struct {
15+ favoritesUC *usecase.ManageFavoritesUseCase
16+}
17+
18+// NewTagHandlers creates a new TagHandlers instance.
19+func NewTagHandlers(favoritesUC *usecase.ManageFavoritesUseCase) *TagHandlers {
20+ return &TagHandlers{favoritesUC: favoritesUC}
21+}
22+
23+// HandleList handles tag_list messages.
24+func (h *TagHandlers) HandleList() webkit.MessageHandler {
25+ return webkit.MessageHandlerFunc(func(ctx context.Context, webviewID webkit.WebViewID, payload json.RawMessage) (any, error) {
26+ log := logging.FromContext(ctx)
27+
28+ requestID := ParseRequestID(payload)
29+
30+ log.Debug().
31+ Str("request_id", requestID).
32+ Msg("handling tag_list")
33+
34+ tags, err := h.favoritesUC.GetAllTags(ctx)
35+ if err != nil {
36+ return NewErrorResponse(requestID, err), nil
37+ }
38+
39+ return NewSuccessResponse(requestID, tags), nil
40+ })
41+}
42+
43+// createTagRequest is the payload for tag_create messages.
44+type createTagRequest struct {
45+ RequestID string `json:"requestId"`
46+ Name string `json:"name"`
47+ Color *string `json:"color"`
48+}
49+
50+// HandleCreate handles tag_create messages.
51+func (h *TagHandlers) HandleCreate() webkit.MessageHandler {
52+ return webkit.MessageHandlerFunc(func(ctx context.Context, webviewID webkit.WebViewID, payload json.RawMessage) (any, error) {
53+ log := logging.FromContext(ctx)
54+
55+ var req createTagRequest
56+ if err := json.Unmarshal(payload, &req); err != nil {
57+ return NewErrorResponse("", err), nil
58+ }
59+
60+ log.Debug().
61+ Str("request_id", req.RequestID).
62+ Str("name", req.Name).
63+ Msg("handling tag_create")
64+
65+ color := ""
66+ if req.Color != nil {
67+ color = *req.Color
68+ }
69+
70+ tag, err := h.favoritesUC.AddTag(ctx, req.Name, color)
71+ if err != nil {
72+ return NewErrorResponse(req.RequestID, err), nil
73+ }
74+
75+ return NewSuccessResponse(req.RequestID, tag), nil
76+ })
77+}
78+
79+// deleteTagRequest is the payload for tag_delete messages.
80+type deleteTagRequest struct {
81+ RequestID string `json:"requestId"`
82+ ID int64 `json:"id"`
83+}
84+
85+// HandleDelete handles tag_delete messages.
86+func (h *TagHandlers) HandleDelete() webkit.MessageHandler {
87+ return webkit.MessageHandlerFunc(func(ctx context.Context, webviewID webkit.WebViewID, payload json.RawMessage) (any, error) {
88+ log := logging.FromContext(ctx)
89+
90+ var req deleteTagRequest
91+ if err := json.Unmarshal(payload, &req); err != nil {
92+ return NewErrorResponse("", err), nil
93+ }
94+
95+ log.Debug().
96+ Str("request_id", req.RequestID).
97+ Int64("id", req.ID).
98+ Msg("handling tag_delete")
99+
100+ if err := h.favoritesUC.DeleteTag(ctx, entity.TagID(req.ID)); err != nil {
101+ return NewErrorResponse(req.RequestID, err), nil
102+ }
103+
104+ return NewSuccessResponse(req.RequestID, nil), nil
105+ })
106+}
107+
108+// updateTagRequest is the payload for tag_update messages.
109+type updateTagRequest struct {
110+ RequestID string `json:"requestId"`
111+ ID int64 `json:"id"`
112+ Name *string `json:"name"`
113+ Color *string `json:"color"`
114+}
115+
116+// HandleUpdate handles tag_update messages.
117+// NOTE: This requires UpdateTag() method to be added to ManageFavoritesUseCase.
118+func (h *TagHandlers) HandleUpdate() webkit.MessageHandler {
119+ return webkit.MessageHandlerFunc(func(ctx context.Context, webviewID webkit.WebViewID, payload json.RawMessage) (any, error) {
120+ log := logging.FromContext(ctx)
121+
122+ var req updateTagRequest
123+ if err := json.Unmarshal(payload, &req); err != nil {
124+ return NewErrorResponse("", err), nil
125+ }
126+
127+ log.Debug().
128+ Str("request_id", req.RequestID).
129+ Int64("id", req.ID).
130+ Msg("handling tag_update")
131+
132+ name := ""
133+ if req.Name != nil {
134+ name = *req.Name
135+ }
136+ color := ""
137+ if req.Color != nil {
138+ color = *req.Color
139+ }
140+
141+ if err := h.favoritesUC.UpdateTag(ctx, entity.TagID(req.ID), name, color); err != nil {
142+ return NewErrorResponse(req.RequestID, err), nil
143+ }
144+
145+ return NewSuccessResponse(req.RequestID, nil), nil
146+ })
147+}
148+
149+// tagAssignRequest is the payload for tag_assign messages.
150+type tagAssignRequest struct {
151+ RequestID string `json:"requestId"`
152+ FavoriteID int64 `json:"favorite_id"`
153+ TagID int64 `json:"tag_id"`
154+}
155+
156+// HandleAssign handles tag_assign messages.
157+func (h *TagHandlers) HandleAssign() webkit.MessageHandler {
158+ return webkit.MessageHandlerFunc(func(ctx context.Context, webviewID webkit.WebViewID, payload json.RawMessage) (any, error) {
159+ log := logging.FromContext(ctx)
160+
161+ var req tagAssignRequest
162+ if err := json.Unmarshal(payload, &req); err != nil {
163+ return NewErrorResponse("", err), nil
164+ }
165+
166+ log.Debug().
167+ Str("request_id", req.RequestID).
168+ Int64("favorite_id", req.FavoriteID).
169+ Int64("tag_id", req.TagID).
170+ Msg("handling tag_assign")
171+
172+ if err := h.favoritesUC.TagFavorite(ctx, entity.FavoriteID(req.FavoriteID), entity.TagID(req.TagID)); err != nil {
173+ return NewErrorResponse(req.RequestID, err), nil
174+ }
175+
176+ return NewSuccessResponse(req.RequestID, nil), nil
177+ })
178+}
179+
180+// HandleRemove handles tag_remove messages.
181+func (h *TagHandlers) HandleRemove() webkit.MessageHandler {
182+ return webkit.MessageHandlerFunc(func(ctx context.Context, webviewID webkit.WebViewID, payload json.RawMessage) (any, error) {
183+ log := logging.FromContext(ctx)
184+
185+ var req tagAssignRequest // same structure as assign
186+ if err := json.Unmarshal(payload, &req); err != nil {
187+ return NewErrorResponse("", err), nil
188+ }
189+
190+ log.Debug().
191+ Str("request_id", req.RequestID).
192+ Int64("favorite_id", req.FavoriteID).
193+ Int64("tag_id", req.TagID).
194+ Msg("handling tag_remove")
195+
196+ if err := h.favoritesUC.UntagFavorite(ctx, entity.FavoriteID(req.FavoriteID), entity.TagID(req.TagID)); err != nil {
197+ return NewErrorResponse(req.RequestID, err), nil
198+ }
199+
200+ return NewSuccessResponse(req.RequestID, nil), nil
201+ })
202+}
A internal/infrastructure/webkit/handlers/register.go +33 -0
@@ -0,0 +1,33 @@
1+package handlers
2+
3+import (
4+ "context"
5+
6+ "github.com/bnema/dumber/internal/application/usecase"
7+ "github.com/bnema/dumber/internal/infrastructure/webkit"
8+ "github.com/bnema/dumber/internal/infrastructure/webkit/handlers/homepage"
9+)
10+
11+// Config holds all dependencies for message handlers.
12+type Config struct {
13+ HistoryUC *usecase.SearchHistoryUseCase
14+ FavoritesUC *usecase.ManageFavoritesUseCase
15+}
16+
17+// RegisterAll registers all message handlers with the router.
18+func RegisterAll(ctx context.Context, router *webkit.MessageRouter, cfg Config) error {
19+ // Homepage handlers (history, favorites, folders, tags)
20+ if err := homepage.RegisterHandlers(ctx, router, homepage.Config{
21+ HistoryUC: cfg.HistoryUC,
22+ FavoritesUC: cfg.FavoritesUC,
23+ }); err != nil {
24+ return err
25+ }
26+
27+ // Future handler groups go here:
28+ // - settings handlers
29+ // - downloads handlers
30+ // - etc.
31+
32+ return nil
33+}
@@ -0,0 +1,33 @@
1+package handlers
2+
3+import (
4+ "context"
5+
6+ "github.com/bnema/dumber/internal/application/usecase"
7+ "github.com/bnema/dumber/internal/infrastructure/webkit"
8+ "github.com/bnema/dumber/internal/infrastructure/webkit/handlers/homepage"
9+)
10+
11+// Config holds all dependencies for message handlers.
12+type Config struct {
13+ HistoryUC *usecase.SearchHistoryUseCase
14+ FavoritesUC *usecase.ManageFavoritesUseCase
15+}
16+
17+// RegisterAll registers all message handlers with the router.
18+func RegisterAll(ctx context.Context, router *webkit.MessageRouter, cfg Config) error {
19+ // Homepage handlers (history, favorites, folders, tags)
20+ if err := homepage.RegisterHandlers(ctx, router, homepage.Config{
21+ HistoryUC: cfg.HistoryUC,
22+ FavoritesUC: cfg.FavoritesUC,
23+ }); err != nil {
24+ return err
25+ }
26+
27+ // Future handler groups go here:
28+ // - settings handlers
29+ // - downloads handlers
30+ // - etc.
31+
32+ return nil
33+}

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