1package component
2
3import (
4 "fmt"
5 "strconv"
6 "strings"
7
8 "github.com/bnema/puregotk/v4/glib"
9 "github.com/bnema/puregotk/v4/gtk"
10
11 "github.com/bnema/dumber/internal/application/dto"
12 "github.com/bnema/dumber/internal/domain/entity"
13)
14
15type favoritesSidebarMode int
16
17const (
18 favoritesSidebarModeNone favoritesSidebarMode = iota
19 favoritesSidebarModeAdd
20 favoritesSidebarModeEdit
21 favoritesSidebarModeCreateTag
22 favoritesSidebarModeBindTag
23 favoritesSidebarModeShortcut
24)
25
26type favoritesSidebarFocusZone int
27
28const (
29 favoritesSidebarFocusSearch favoritesSidebarFocusZone = iota
30 favoritesSidebarFocusTags
31 favoritesSidebarFocusList
32 favoritesSidebarFocusForm
33 favoritesSidebarFocusConfirm
34)
35
36func (fs *FavoritesSidebar) beginAddForm() {
37 fs.mu.Lock()
38 if fs.destroyed {
39 fs.mu.Unlock()
40 return
41 }
42 fs.mode = favoritesSidebarModeAdd
43 fs.editingID = 0
44 fs.confirmDelete = false
45 fs.confirmDeleteID = 0
46 fs.setNoticeLocked("Add favorite: URL, title, tags, shortcut 1-9. Press Ctrl+Enter or Save.")
47 fs.mu.Unlock()
48 fs.renderForm(nil)
49 fs.rebuildList()
50}
51
52func (fs *FavoritesSidebar) beginEditForm() {
53 fav := fs.selectedFavorite()
54 if fav == nil {
55 fs.setNotice("Select a favorite to edit")
56 return
57 }
58 fs.mu.Lock()
59 if fs.destroyed {
60 fs.mu.Unlock()
61 return
62 }
63 fs.mode = favoritesSidebarModeEdit
64 fs.editingID = fav.ID
65 fs.confirmDelete = false
66 fs.confirmDeleteID = 0
67 fs.setNoticeLocked("Edit favorite: URL is read-only; use + on the selected favorite to manage tags. Press Ctrl+Enter or Save.")
68 fs.mu.Unlock()
69 fs.renderForm(fav)
70 fs.rebuildList()
71}
72
73func (fs *FavoritesSidebar) renderForm(fav *entity.Favorite) {
74 if fs == nil || fs.formBox == nil {
75 return
76 }
77 clearBoxChildren(fs.formBox)
78 fs.formCallbacks = nil
79 fs.formTagMatchCallbacks = nil
80 fs.formURLEntry = gtk.NewSearchEntry()
81 fs.formTitleEntry = gtk.NewSearchEntry()
82 fs.formTagSearch = gtk.NewSearchEntry()
83 fs.formTagMatches = gtk.NewBox(gtk.OrientationHorizontalValue, 3)
84 fs.formShortcutEntry = gtk.NewSearchEntry()
85 fs.mu.Lock()
86 fs.formTagIDs = tagIDSet(favoriteTags(fav))
87 fs.mu.Unlock()
88 entries := []struct {
89 entry *gtk.SearchEntry
90 label string
91 }{
92 {entry: fs.formURLEntry, label: "URL"},
93 {entry: fs.formTitleEntry, label: "Title"},
94 {entry: fs.formTagSearch, label: "Add tags..."},
95 {entry: fs.formShortcutEntry, label: "Shortcut"},
96 }
97 for _, item := range entries {
98 if item.entry == nil {
99 continue
100 }
101 placeholder := item.label
102 item.entry.SetPlaceholderText(&placeholder)
103 fs.trackTextInputFocus(&item.entry.Widget, &fs.formCallbacks)
104 fs.formBox.Append(&item.entry.Widget)
105 }
106 if fs.formTagMatches != nil {
107 fs.formTagMatches.AddCssClass("favorites-sidebar-form-tag-matches")
108 fs.formBox.Append(&fs.formTagMatches.Widget)
109 }
110 fs.setupFormTagSearch()
111 fs.formSaveButton = gtk.NewButtonWithLabel("Save")
112 if fs.formSaveButton != nil {
113 cb := func(_ gtk.Button) {
114 fs.submitForm()
115 }
116 fs.formCallbacks = append(fs.formCallbacks, cb)
117 fs.formSaveButton.ConnectClicked(&cb)
118 fs.formBox.Append(&fs.formSaveButton.Widget)
119 }
120 if fav != nil {
121 fs.formURL = fav.URL
122 fs.formTitle = fav.Title
123 fs.formShortcut = ""
124 if fav.ShortcutKey != nil {
125 fs.formShortcut = strconv.Itoa(*fav.ShortcutKey)
126 }
127 if fs.formURLEntry != nil {
128 fs.formURLEntry.SetText(fs.formURL)
129 fs.formURLEntry.SetEditable(false)
130 }
131 if fs.formTitleEntry != nil {
132 fs.formTitleEntry.SetText(fs.formTitle)
133 }
134 if fs.formShortcutEntry != nil {
135 fs.formShortcutEntry.SetText(fs.formShortcut)
136 }
137 } else {
138 fs.formURL, fs.formTitle, fs.formShortcut = "", "", ""
139 }
140 fs.renderFormTagMatches("")
141 fs.formBox.SetVisible(true)
142 fs.focusForm()
143}
144
145func (fs *FavoritesSidebar) focusForm() {
146 fs.scheduleIdle(glib.SourceFunc(func(uintptr) bool {
147 if fs == nil {
148 return false
149 }
150 fs.mu.RLock()
151 mode := fs.mode
152 destroyed := fs.destroyed
153 fs.mu.RUnlock()
154 if destroyed {
155 return false
156 }
157 if mode == favoritesSidebarModeEdit && fs.formTitleEntry != nil {
158 fs.formTitleEntry.GrabFocus()
159 return false
160 }
161 if fs.formURLEntry != nil {
162 fs.formURLEntry.GrabFocus()
163 }
164 return false
165 }))
166}
167
168func (fs *FavoritesSidebar) cancelManagement() bool {
169 fs.mu.Lock()
170 if fs.destroyed {
171 fs.mu.Unlock()
172 return false
173 }
174 active := fs.mode != favoritesSidebarModeNone || fs.confirmDelete
175 fs.mode = favoritesSidebarModeNone
176 fs.editingID = 0
177 fs.confirmDelete = false
178 fs.confirmDeleteID = 0
179 if active {
180 fs.setNoticeLocked("")
181 }
182 fs.mu.Unlock()
183 if fs.formBox != nil {
184 fs.formBox.SetVisible(false)
185 clearBoxChildren(fs.formBox)
186 fs.formCallbacks = nil
187 fs.formTagMatchCallbacks = nil
188 }
189 if fs.tagPromptBox != nil {
190 fs.hideTagPrompt()
191 }
192 fs.rebuildList()
193 return active
194}
195
196func (fs *FavoritesSidebar) submitForm() bool {
197 if fs == nil {
198 return false
199 }
200 fs.mu.RLock()
201 mode := fs.mode
202 id := fs.editingID
203 uc := fs.favoritesUC
204 ctx := fs.ctx
205 fs.mu.RUnlock()
206 if uc == nil || (mode != favoritesSidebarModeAdd && mode != favoritesSidebarModeEdit) {
207 return false
208 }
209 url, title, shortcutText := fs.formValues()
210 tags := fs.formTagIDsSnapshot()
211 if mode == favoritesSidebarModeAdd {
212 key, err := parseShortcut(shortcutText)
213 if err != nil {
214 fs.setNotice(err.Error())
215 return true
216 }
217 input := dto.FavoriteCreateInput{URL: url, Title: title, Tags: tags}
218 fav, err := uc.AddFavorite(ctx, input)
219 if err != nil {
220 fs.setNotice(err.Error())
221 return true
222 }
223 if key != nil && fav != nil {
224 if err := uc.SetShortcut(ctx, fav.ID, key); err != nil {
225 fs.cancelManagement()
226 fs.startLoad()
227 fs.setNotice("Added favorite, but failed to set shortcut: " + err.Error())
228 return true
229 }
230 }
231 } else {
232 key, err := parseShortcut(shortcutText)
233 if err != nil {
234 fs.setNotice(err.Error())
235 return true
236 }
237 _, err = uc.UpdateFavorite(ctx, dto.FavoriteUpdateInput{ID: id, Title: title, ShortcutKey: key, ShortcutKeySet: true})
238 if err != nil {
239 fs.setNotice(err.Error())
240 return true
241 }
242 if !fs.updateFavoriteTags(id, tags) {
243 return true
244 }
245 }
246 fs.cancelManagement()
247 fs.startLoad()
248 return true
249}
250
251func (fs *FavoritesSidebar) enterShortcutMode() {
252 fs.setModeNotice(favoritesSidebarModeShortcut, "Shortcut mode: press 1-9 to assign, Backspace/Delete to clear")
253}
254
255func (fs *FavoritesSidebar) setModeNotice(mode favoritesSidebarMode, notice string) {
256 fs.mu.Lock()
257 if !fs.destroyed {
258 fs.mode = mode
259 fs.confirmDelete = false
260 fs.confirmDeleteID = 0
261 fs.setNoticeLocked(notice)
262 }
263 fs.mu.Unlock()
264 fs.rebuildList()
265}
266
267func (fs *FavoritesSidebar) setShortcutKey(key *int) bool {
268 fav := fs.selectedFavorite()
269 if fav == nil {
270 fs.setNotice("Select a favorite for shortcut")
271 return true
272 }
273 fs.mu.RLock()
274 uc := fs.favoritesUC
275 ctx := fs.ctx
276 fs.mu.RUnlock()
277 if uc == nil {
278 return true
279 }
280 if err := uc.SetShortcut(ctx, fav.ID, key); err != nil {
281 fs.setNotice(err.Error())
282 return true
283 }
284 fs.startLoad()
285 return true
286}
287
288func (fs *FavoritesSidebar) requestDeleteConfirmation() bool {
289 fav := fs.selectedFavorite()
290 if fav == nil {
291 fs.setNotice("Select a favorite to delete")
292 return true
293 }
294 fs.mu.Lock()
295 if !fs.destroyed {
296 fs.confirmDelete = true
297 fs.confirmDeleteID = fav.ID
298 fs.setNoticeLocked("Delete selected favorite? Press Delete again to confirm, Esc to cancel.")
299 }
300 fs.mu.Unlock()
301 fs.rebuildList()
302 return true
303}
304
305func (fs *FavoritesSidebar) formModeActive() bool {
306 if fs == nil {
307 return false
308 }
309 fs.mu.RLock()
310 defer fs.mu.RUnlock()
311 return fs.mode == favoritesSidebarModeAdd || fs.mode == favoritesSidebarModeEdit
312}
313
314func (fs *FavoritesSidebar) confirmDeleteActive() bool {
315 if fs == nil {
316 return false
317 }
318 fs.mu.RLock()
319 defer fs.mu.RUnlock()
320 return fs.confirmDelete
321}
322
323func (fs *FavoritesSidebar) confirmDeleteFavorite() bool {
324 fav := fs.selectedFavorite()
325 if fav == nil {
326 return fs.requestDeleteConfirmation()
327 }
328 fs.mu.RLock()
329 confirmed := fs.confirmDelete
330 confirmedID := fs.confirmDeleteID
331 uc := fs.favoritesUC
332 ctx := fs.ctx
333 fs.mu.RUnlock()
334 if !confirmed || confirmedID != fav.ID {
335 return fs.requestDeleteConfirmation()
336 }
337 if uc == nil {
338 return true
339 }
340 if err := uc.DeleteFavorite(ctx, fav.ID); err != nil {
341 fs.setNotice(err.Error())
342 return true
343 }
344 fs.cancelManagement()
345 fs.startLoad()
346 return true
347}
348
349func (fs *FavoritesSidebar) selectedFavorite() *entity.Favorite {
350 fs.mu.RLock()
351 defer fs.mu.RUnlock()
352 if len(fs.displayRows) == 1 {
353 return fs.displayRows[0].Favorite
354 }
355 idx := firstSelectableIndex(fs.displayRows)
356 if fs.listBox != nil {
357 if row := fs.listBox.GetSelectedRow(); row != nil {
358 idx = row.GetIndex()
359 }
360 }
361 if idx >= 0 && idx < len(fs.displayRows) {
362 return fs.displayRows[idx].Favorite
363 }
364 return nil
365}
366
367func (fs *FavoritesSidebar) setNotice(notice string) {
368 fs.mu.Lock()
369 if !fs.destroyed {
370 fs.setNoticeLocked(notice)
371 }
372 fs.mu.Unlock()
373 fs.rebuildList()
374}
375
376func (fs *FavoritesSidebar) formValues() (string, string, string) {
377 url, title, shortcut := fs.formURL, fs.formTitle, fs.formShortcut
378 if fs.formURLEntry != nil {
379 url = fs.formURLEntry.GetText()
380 }
381 if fs.formTitleEntry != nil {
382 title = fs.formTitleEntry.GetText()
383 }
384 if fs.formShortcutEntry != nil {
385 shortcut = fs.formShortcutEntry.GetText()
386 }
387 return strings.TrimSpace(url), strings.TrimSpace(title), strings.TrimSpace(shortcut)
388}
389
390func parseShortcut(text string) (*int, error) {
391 text = strings.TrimSpace(text)
392 if text == "" {
393 return nil, nil
394 }
395 v, err := strconv.Atoi(text)
396 if err != nil || v < 1 || v > 9 {
397 return nil, fmt.Errorf("invalid shortcut %q (use 1-9)", text)
398 }
399 return &v, nil
400}