detached 3aefd86

feat(homepage): add interactive history deletion with lucide icons and enhanced UX

commit
3aefd869dc1f300e98b541421bad1846ae209053
author
bamen · · 2025-09-15 09:17 UTC
parent
2e766c7
signature
unsigned
9 files · +411 -63
M CHANGELOG.md +5 -0
@@ -11,6 +11,8 @@ All notable changes to this project will be documented in this file.
1111 - **Click-anywhere-to-close**: Omnibox closes when clicking outside for better UX
1212 - **Homepage Svelte 5 component**: New standalone homepage with history display and keyboard shortcuts reference
1313 - **Page generator system**: Vite plugin for generating dumb:// protocol pages (homepage, config, about) with extensible architecture
14+- **History deletion API**: New DELETE endpoint at `/history/delete?id=N` for removing individual history entries
15+- **Lucide icons integration**: Professional SVG icons replacing emoji throughout the UI
1416
1517 ### Improved
1618 - **Omnibox/Find system**: Complete migration from 500-line raw JavaScript to modular Svelte 5 components
@@ -18,6 +20,9 @@ All notable changes to this project will be documented in this file.
1820 - **GUI build pipeline**: Vite-based bundling with hot reload and TypeScript support
1921 - **Keyboard shortcuts**: Fixed Ctrl+F not working by adding missing 'f' key to keyboard dispatcher
2022 - **State management**: Proper cleanup between omnibox and find modes prevents state conflicts
23+- **Homepage UX**: Enhanced layout with 70/30 flex ratio, compact keyboard shortcuts table, and statistics display
24+- **Interactive delete**: History entries show trash icon on hover with smooth fade-out animation on deletion
25+- **Top 5 most visited**: New section displaying frequently accessed sites with visit counts
2126 - **Performance**: Instant shortcut response with pre-mounted DOM elements
2227
2328 ### Fixed
@@ -11,6 +11,8 @@ All notable changes to this project will be documented in this file.
11 - **Click-anywhere-to-close**: Omnibox closes when clicking outside for better UX11 - **Click-anywhere-to-close**: Omnibox closes when clicking outside for better UX
12 - **Homepage Svelte 5 component**: New standalone homepage with history display and keyboard shortcuts reference12 - **Homepage Svelte 5 component**: New standalone homepage with history display and keyboard shortcuts reference
13 - **Page generator system**: Vite plugin for generating dumb:// protocol pages (homepage, config, about) with extensible architecture13 - **Page generator system**: Vite plugin for generating dumb:// protocol pages (homepage, config, about) with extensible architecture
14+- **History deletion API**: New DELETE endpoint at `/history/delete?id=N` for removing individual history entries
15+- **Lucide icons integration**: Professional SVG icons replacing emoji throughout the UI
14 16
15 ### Improved17 ### Improved
16 - **Omnibox/Find system**: Complete migration from 500-line raw JavaScript to modular Svelte 5 components18 - **Omnibox/Find system**: Complete migration from 500-line raw JavaScript to modular Svelte 5 components
@@ -18,6 +20,9 @@ All notable changes to this project will be documented in this file.
18 - **GUI build pipeline**: Vite-based bundling with hot reload and TypeScript support20 - **GUI build pipeline**: Vite-based bundling with hot reload and TypeScript support
19 - **Keyboard shortcuts**: Fixed Ctrl+F not working by adding missing 'f' key to keyboard dispatcher21 - **Keyboard shortcuts**: Fixed Ctrl+F not working by adding missing 'f' key to keyboard dispatcher
20 - **State management**: Proper cleanup between omnibox and find modes prevents state conflicts22 - **State management**: Proper cleanup between omnibox and find modes prevents state conflicts
23+- **Homepage UX**: Enhanced layout with 70/30 flex ratio, compact keyboard shortcuts table, and statistics display
24+- **Interactive delete**: History entries show trash icon on hover with smooth fade-out animation on deletion
25+- **Top 5 most visited**: New section displaying frequently accessed sites with visit counts
21 - **Performance**: Instant shortcut response with pre-mounted DOM elements26 - **Performance**: Instant shortcut response with pre-mounted DOM elements
22 27
23 ### Fixed28 ### Fixed
M gui/src/pages/Homepage.svelte +367 -58
@@ -1,5 +1,6 @@
11 <script lang="ts">
22 import { onMount } from 'svelte';
3+ import { Trash2, Globe } from '@lucide/svelte';
34
45 interface HistoryItem {
56 id: number;
@@ -17,10 +18,22 @@
1718 action: () => void;
1819 }
1920
21+ interface HistoryStats {
22+ total_entries: number;
23+ recent_count: number;
24+ oldest_entry?: string;
25+ newest_entry?: string;
26+ }
27+
2028 let historyItems = $state<HistoryItem[]>([]);
29+ let topVisited = $state<HistoryItem[]>([]);
2130 let shortcuts = $state<Shortcut[]>([]);
31+ let stats = $state<HistoryStats | null>(null);
2232 let historyLoading = $state(true);
33+ let topVisitedLoading = $state(true);
2334 let shortcutsLoading = $state(true);
35+ let statsLoading = $state(true);
36+ let deletingIds = $state<Set<number>>(new Set());
2437
2538 // Initialize shortcuts (these are browser shortcuts, not dynamic from API)
2639 const initializeShortcuts = () => {
@@ -28,49 +41,62 @@
2841 {
2942 key: 'Ctrl+L',
3043 description: 'Focus address bar',
31- action: () => {
32- // This will be handled by the browser's keyboard shortcuts
33- console.log('Ctrl+L triggered');
34- }
44+ action: () => console.log('Ctrl+L triggered')
3545 },
3646 {
3747 key: 'Ctrl+F',
3848 description: 'Find in page',
39- action: () => {
40- // This will be handled by the browser's keyboard shortcuts
41- console.log('Ctrl+F triggered');
42- }
49+ action: () => console.log('Ctrl+F triggered')
50+ },
51+ {
52+ key: 'Ctrl+Shift+C',
53+ description: 'Copy URL',
54+ action: () => console.log('Ctrl+Shift+C triggered')
4355 },
4456 {
4557 key: 'Ctrl+R',
4658 description: 'Reload page',
47- action: () => {
48- window.location.reload();
49- }
59+ action: () => window.location.reload()
60+ },
61+ {
62+ key: 'Ctrl+Shift+R',
63+ description: 'Hard reload',
64+ action: () => console.log('Ctrl+Shift+R triggered')
65+ },
66+ {
67+ key: 'F5',
68+ description: 'Reload page',
69+ action: () => window.location.reload()
70+ },
71+ {
72+ key: 'F12',
73+ description: 'DevTools',
74+ action: () => console.log('F12 triggered')
75+ },
76+ {
77+ key: 'Alt+←',
78+ description: 'Navigate back',
79+ action: () => console.log('Alt+← triggered')
80+ },
81+ {
82+ key: 'Alt+→',
83+ description: 'Navigate forward',
84+ action: () => console.log('Alt+→ triggered')
5085 },
5186 {
5287 key: 'Ctrl+0',
5388 description: 'Reset zoom',
54- action: () => {
55- // This will be handled by the browser's zoom controls
56- console.log('Ctrl+0 triggered');
57- }
89+ action: () => console.log('Ctrl+0 triggered')
5890 },
5991 {
6092 key: 'Ctrl++',
6193 description: 'Zoom in',
62- action: () => {
63- // This will be handled by the browser's zoom controls
64- console.log('Ctrl++ triggered');
65- }
94+ action: () => console.log('Ctrl++ triggered')
6695 },
6796 {
6897 key: 'Ctrl+-',
6998 description: 'Zoom out',
70- action: () => {
71- // This will be handled by the browser's zoom controls
72- console.log('Ctrl+- triggered');
73- }
99+ action: () => console.log('Ctrl+- triggered')
74100 }
75101 ];
76102 shortcutsLoading = false;
@@ -100,6 +126,38 @@
100126 window.location.href = url;
101127 };
102128
129+ // Delete a history entry
130+ const deleteHistoryEntry = async (id: number, event: Event) => {
131+ event.stopPropagation(); // Prevent navigation
132+
133+ // Add to deleting set for animation
134+ deletingIds = new Set([...deletingIds, id]);
135+
136+ try {
137+ const response = await fetch(`dumb://homepage/api/history/delete?id=${id}`);
138+ if (response.ok) {
139+ // Wait a bit for the fade animation to complete
140+ await new Promise(resolve => setTimeout(resolve, 200));
141+
142+ // Refresh history after successful deletion
143+ await fetchHistory();
144+ await fetchStats();
145+ await fetchTopVisited();
146+ } else {
147+ console.error('Failed to delete history entry:', response.status);
148+ // Remove from deleting set if failed
149+ deletingIds = new Set([...deletingIds].filter(deletingId => deletingId !== id));
150+ }
151+ } catch (error) {
152+ console.error('Error deleting history entry:', error);
153+ // Remove from deleting set if failed
154+ deletingIds = new Set([...deletingIds].filter(deletingId => deletingId !== id));
155+ }
156+
157+ // Remove from deleting set after refresh
158+ deletingIds = new Set([...deletingIds].filter(deletingId => deletingId !== id));
159+ };
160+
103161 // Get domain from URL
104162 const getDomain = (url: string): string => {
105163 try {
@@ -131,9 +189,49 @@
131189 }
132190 };
133191
192+ // Fetch history stats from the API
193+ const fetchStats = async () => {
194+ try {
195+ const response = await fetch('dumb://homepage/api/history/stats');
196+ if (response.ok) {
197+ const data = await response.json();
198+ stats = data;
199+ } else {
200+ console.warn('Failed to fetch stats:', response.status);
201+ stats = null;
202+ }
203+ } catch (error) {
204+ console.error('Error fetching stats:', error);
205+ stats = null;
206+ } finally {
207+ statsLoading = false;
208+ }
209+ };
210+
211+ // Fetch top visited sites using search endpoint sorted by visit count
212+ const fetchTopVisited = async () => {
213+ try {
214+ const response = await fetch('dumb://homepage/api/history/search?q=&limit=5');
215+ if (response.ok) {
216+ const data = await response.json();
217+ topVisited = Array.isArray(data) ? data.slice(0, 5) : [];
218+ } else {
219+ console.warn('Failed to fetch top visited:', response.status);
220+ topVisited = [];
221+ }
222+ } catch (error) {
223+ console.error('Error fetching top visited:', error);
224+ topVisited = [];
225+ } finally {
226+ topVisitedLoading = false;
227+ }
228+ };
229+
134230 onMount(() => {
135231 initializeShortcuts();
136232 fetchHistory();
233+ fetchStats();
234+ fetchTopVisited();
137235 });
138236
139237 </script>
@@ -154,6 +252,7 @@
154252 {#each historyItems as item (item.url)}
155253 <div
156254 class="history-item"
255+ class:deleting={deletingIds.has(item.id)}
157256 role="button"
158257 tabindex="0"
159258 onclick={() => navigateTo(item.url)}
@@ -182,9 +281,13 @@
182281 }
183282 }}
184283 />
185- <div class="history-favicon-fallback" style="display: none;">🌐</div>
284+ <div class="history-favicon-fallback" style="display: none;">
285+ <Globe size={12} />
286+ </div>
186287 {:else}
187- <div class="history-favicon-fallback">🌐</div>
288+ <div class="history-favicon-fallback">
289+ <Globe size={12} />
290+ </div>
188291 {/if}
189292 </div>
190293 <div class="history-title">{item.title || 'Untitled'}</div>
@@ -193,6 +296,16 @@
193296 <div class="history-sep">•</div>
194297 <div class="history-url">{item.url}</div>
195298 <div class="history-time">{formatTime(item.last_visited)}</div>
299+ <div
300+ class="history-delete"
301+ role="button"
302+ tabindex="0"
303+ onclick={(e) => deleteHistoryEntry(item.id, e)}
304+ onkeydown={(e) => e.key === 'Enter' && deleteHistoryEntry(item.id, e)}
305+ title="Delete this entry"
306+ >
307+ <Trash2 size={16} />
308+ </div>
196309 </div>
197310 </div>
198311 {/each}
@@ -202,22 +315,68 @@
202315
203316 <div class="shortcuts-section">
204317 <h2 class="section-title">Keyboard Shortcuts</h2>
205- <div class="shortcuts-grid">
318+ <div class="shortcuts-container">
206319 {#if shortcutsLoading}
207320 <div class="loading">Loading shortcuts...</div>
208321 {:else}
209- {#each shortcuts as shortcut (shortcut.key)}
210- <div
211- class="shortcut"
212- role="button"
213- tabindex="0"
214- onclick={shortcut.action}
215- onkeydown={(e) => e.key === 'Enter' && shortcut.action()}
216- >
217- <div class="shortcut-key">{shortcut.key}</div>
218- <div class="shortcut-desc">{shortcut.description}</div>
322+ <table class="shortcuts-table">
323+ <tbody>
324+ {#each shortcuts as shortcut (shortcut.key)}
325+ <tr class="shortcut-row">
326+ <td class="shortcut-key">{shortcut.key}</td>
327+ <td class="shortcut-desc">{shortcut.description}</td>
328+ </tr>
329+ {/each}
330+ </tbody>
331+ </table>
332+ {/if}
333+ </div>
334+
335+ <h2 class="section-title stats-title">Statistics</h2>
336+ <div class="stats-container">
337+ {#if statsLoading}
338+ <div class="loading">Loading stats...</div>
339+ {:else if stats}
340+ <div class="stats-grid">
341+ <div class="stat-item">
342+ <div class="stat-value">{stats.total_entries}</div>
343+ <div class="stat-label">History Entries</div>
219344 </div>
220- {/each}
345+ </div>
346+ {:else}
347+ <div class="empty-state">
348+ <p>No statistics available</p>
349+ </div>
350+ {/if}
351+ </div>
352+
353+ <h2 class="section-title top-visited-title">Top 5 Most Visited</h2>
354+ <div class="top-visited-container">
355+ {#if topVisitedLoading}
356+ <div class="loading">Loading top visited...</div>
357+ {:else if topVisited.length === 0}
358+ <div class="empty-state">
359+ <p>No visit data available</p>
360+ </div>
361+ {:else}
362+ <div class="top-visited-list">
363+ {#each topVisited as item, index (item.url)}
364+ <div
365+ class="top-visited-item"
366+ role="button"
367+ tabindex="0"
368+ onclick={() => navigateTo(item.url)}
369+ onkeydown={(e) => e.key === 'Enter' && navigateTo(item.url)}
370+ >
371+ <div class="rank">{index + 1}</div>
372+ <div class="site-info">
373+ <div class="site-title">{item.title || 'Untitled'}</div>
374+ <div class="site-domain">{getDomain(item.url)}</div>
375+ </div>
376+ <div class="visit-count">{item.visit_count}</div>
377+ </div>
378+ {/each}
379+ </div>
221380 {/if}
222381 </div>
223382 </div>
@@ -258,7 +417,7 @@
258417 }
259418
260419 .history-section {
261- flex: 1;
420+ flex: 2;
262421 min-height: 0;
263422 min-width: 0;
264423 }
@@ -290,10 +449,13 @@
290449 background: #2d2d2d;
291450 border-radius: 6px;
292451 cursor: pointer;
293- transition: background 0.2s;
452+ transition: all 0.2s ease-in-out;
294453 border-left: 3px solid #404040;
295454 overflow: hidden;
296455 max-width: 100%;
456+ position: relative;
457+ opacity: 1;
458+ transform: translateX(0);
297459 }
298460
299461 .history-item:hover {
@@ -301,6 +463,17 @@
301463 border-left-color: #0066cc;
302464 }
303465
466+ .history-item:hover .history-delete {
467+ opacity: 1;
468+ visibility: visible;
469+ }
470+
471+ .history-item.deleting {
472+ opacity: 0;
473+ transform: translateX(-20px);
474+ pointer-events: none;
475+ }
476+
304477 .history-line {
305478 display: flex;
306479 gap: 0.5rem;
@@ -333,6 +506,10 @@
333506
334507 .history-favicon-fallback {
335508 font-size: 12px;
509+ display: flex;
510+ align-items: center;
511+ justify-content: center;
512+ color: #888;
336513 }
337514
338515 .history-title {
@@ -370,40 +547,176 @@
370547 margin-left: auto;
371548 }
372549
373- .shortcuts-grid {
374- display: grid;
375- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
376- gap: 1rem;
550+ .history-delete {
551+ position: absolute;
552+ right: 0.75rem;
553+ top: 50%;
554+ transform: translateY(-50%);
555+ opacity: 0;
556+ visibility: hidden;
557+ transition: opacity 0.2s, visibility 0.2s, transform 0.2s;
558+ cursor: pointer;
559+ background: #1a1a1a;
560+ border-radius: 3px;
561+ padding: 0.25rem;
562+ border: 1px solid #404040;
563+ display: flex;
564+ align-items: center;
565+ justify-content: center;
566+ width: 2rem;
567+ height: 2rem;
568+ color: #888;
569+ }
570+
571+ .history-delete:hover {
572+ background: #ff4444;
573+ border-color: #ff6666;
574+ color: #fff;
575+ transform: translateY(-50%) scale(1.1);
576+ }
577+
578+ .shortcuts-container {
377579 max-height: calc(100vh - 8rem);
378580 overflow-y: auto;
379581 }
380582
381- .shortcut {
583+ .shortcuts-table {
584+ width: 100%;
585+ border-collapse: collapse;
586+ background: #2d2d2d;
587+ border-radius: 6px;
588+ overflow: hidden;
589+ }
590+
591+ .shortcut-row {
592+ border-bottom: 1px solid #404040;
593+ transition: background 0.2s;
594+ }
595+
596+ .shortcut-row:last-child {
597+ border-bottom: none;
598+ }
599+
600+ .shortcut-row:hover {
601+ background: #3d3d3d;
602+ }
603+
604+ .shortcut-key {
605+ padding: 0.4rem 0.8rem;
606+ font-weight: bold;
607+ color: #0066cc;
608+ font-size: 0.85rem;
609+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
610+ white-space: nowrap;
611+ width: 1%;
612+ }
613+
614+ .shortcut-desc {
615+ padding: 0.4rem 0.8rem;
616+ color: #e6e6e6;
617+ font-size: 0.85rem;
618+ }
619+
620+ .stats-title {
621+ margin-top: 2rem;
622+ }
623+
624+ .stats-container {
625+ margin-top: 1rem;
626+ }
627+
628+ .stats-grid {
629+ display: flex;
630+ gap: 1rem;
631+ }
632+
633+ .stat-item {
634+ background: #2d2d2d;
635+ border-radius: 6px;
382636 padding: 1rem;
637+ text-align: center;
638+ flex: 1;
639+ border: 1px solid #404040;
640+ }
641+
642+ .stat-value {
643+ font-size: 1.5rem;
644+ font-weight: bold;
645+ color: #0066cc;
646+ margin-bottom: 0.25rem;
647+ }
648+
649+ .stat-label {
650+ font-size: 0.8rem;
651+ color: #888;
652+ text-transform: uppercase;
653+ letter-spacing: 0.5px;
654+ }
655+
656+ .top-visited-title {
657+ margin-top: 2rem;
658+ }
659+
660+ .top-visited-container {
661+ margin-top: 1rem;
662+ }
663+
664+ .top-visited-list {
665+ display: flex;
666+ flex-direction: column;
667+ gap: 0.5rem;
668+ }
669+
670+ .top-visited-item {
671+ display: flex;
672+ align-items: center;
673+ padding: 0.75rem;
383674 background: #2d2d2d;
384- border: 2px solid #404040;
385- border-radius: 8px;
675+ border-radius: 6px;
386676 cursor: pointer;
387- transition: all 0.2s;
388- text-align: center;
677+ transition: background 0.2s;
678+ border: 1px solid #404040;
679+ gap: 1rem;
389680 }
390681
391- .shortcut:hover {
682+ .top-visited-item:hover {
392683 background: #3d3d3d;
393684 border-color: #0066cc;
394- transform: translateY(-2px);
395685 }
396686
397- .shortcut-key {
687+ .rank {
398688 font-weight: bold;
399689 color: #0066cc;
400- margin-bottom: 0.5rem;
401690 font-size: 1.1rem;
691+ min-width: 1.5rem;
692+ text-align: center;
402693 }
403694
404- .shortcut-desc {
405- color: #888;
695+ .site-info {
696+ flex: 1;
697+ min-width: 0;
698+ }
699+
700+ .site-title {
406701 font-size: 0.9rem;
702+ color: #e6e6e6;
703+ margin-bottom: 0.2rem;
704+ white-space: nowrap;
705+ overflow: hidden;
706+ text-overflow: ellipsis;
707+ }
708+
709+ .site-domain {
710+ font-size: 0.8rem;
711+ color: #888;
712+ }
713+
714+ .visit-count {
715+ font-size: 0.85rem;
716+ color: #0066cc;
717+ font-weight: bold;
718+ min-width: 2rem;
719+ text-align: center;
407720 }
408721
409722 .loading {
@@ -428,9 +741,5 @@
428741 flex-direction: column;
429742 padding: 1rem;
430743 }
431-
432- .shortcuts-grid {
433- grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
434- }
435744 }
436745 </style>⏎̸
@@ -1,5 +1,6 @@
1 <script lang="ts">1 <script lang="ts">
2 import { onMount } from 'svelte';2 import { onMount } from 'svelte';
3+ import { Trash2, Globe } from '@lucide/svelte';
3 4
4 interface HistoryItem {5 interface HistoryItem {
5 id: number;6 id: number;
@@ -17,10 +18,22 @@
17 action: () => void;18 action: () => void;
18 }19 }
19 20
21+ interface HistoryStats {
22+ total_entries: number;
23+ recent_count: number;
24+ oldest_entry?: string;
25+ newest_entry?: string;
26+ }
27+
20 let historyItems = $state<HistoryItem[]>([]);28 let historyItems = $state<HistoryItem[]>([]);
29+ let topVisited = $state<HistoryItem[]>([]);
21 let shortcuts = $state<Shortcut[]>([]);30 let shortcuts = $state<Shortcut[]>([]);
31+ let stats = $state<HistoryStats | null>(null);
22 let historyLoading = $state(true);32 let historyLoading = $state(true);
33+ let topVisitedLoading = $state(true);
23 let shortcutsLoading = $state(true);34 let shortcutsLoading = $state(true);
35+ let statsLoading = $state(true);
36+ let deletingIds = $state<Set<number>>(new Set());
24 37
25 // Initialize shortcuts (these are browser shortcuts, not dynamic from API)38 // Initialize shortcuts (these are browser shortcuts, not dynamic from API)
26 const initializeShortcuts = () => {39 const initializeShortcuts = () => {
@@ -28,49 +41,62 @@
28 {41 {
29 key: 'Ctrl+L',42 key: 'Ctrl+L',
30 description: 'Focus address bar',43 description: 'Focus address bar',
31- action: () => {44+ action: () => console.log('Ctrl+L triggered')
32- // This will be handled by the browser's keyboard shortcuts
33- console.log('Ctrl+L triggered');
34- }
35 },45 },
36 {46 {
37 key: 'Ctrl+F',47 key: 'Ctrl+F',
38 description: 'Find in page',48 description: 'Find in page',
39- action: () => {49+ action: () => console.log('Ctrl+F triggered')
40- // This will be handled by the browser's keyboard shortcuts50+ },
41- console.log('Ctrl+F triggered');51+ {
42- }52+ key: 'Ctrl+Shift+C',
53+ description: 'Copy URL',
54+ action: () => console.log('Ctrl+Shift+C triggered')
43 },55 },
44 {56 {
45 key: 'Ctrl+R',57 key: 'Ctrl+R',
46 description: 'Reload page',58 description: 'Reload page',
47- action: () => {59+ action: () => window.location.reload()
48- window.location.reload();60+ },
49- }61+ {
62+ key: 'Ctrl+Shift+R',
63+ description: 'Hard reload',
64+ action: () => console.log('Ctrl+Shift+R triggered')
65+ },
66+ {
67+ key: 'F5',
68+ description: 'Reload page',
69+ action: () => window.location.reload()
70+ },
71+ {
72+ key: 'F12',
73+ description: 'DevTools',
74+ action: () => console.log('F12 triggered')
75+ },
76+ {
77+ key: 'Alt+←',
78+ description: 'Navigate back',
79+ action: () => console.log('Alt+← triggered')
80+ },
81+ {
82+ key: 'Alt+→',
83+ description: 'Navigate forward',
84+ action: () => console.log('Alt+→ triggered')
50 },85 },
51 {86 {
52 key: 'Ctrl+0',87 key: 'Ctrl+0',
53 description: 'Reset zoom',88 description: 'Reset zoom',
54- action: () => {89+ action: () => console.log('Ctrl+0 triggered')
55- // This will be handled by the browser's zoom controls
56- console.log('Ctrl+0 triggered');
57- }
58 },90 },
59 {91 {
60 key: 'Ctrl++',92 key: 'Ctrl++',
61 description: 'Zoom in',93 description: 'Zoom in',
62- action: () => {94+ action: () => console.log('Ctrl++ triggered')
63- // This will be handled by the browser's zoom controls
64- console.log('Ctrl++ triggered');
65- }
66 },95 },
67 {96 {
68 key: 'Ctrl+-',97 key: 'Ctrl+-',
69 description: 'Zoom out',98 description: 'Zoom out',
70- action: () => {99+ action: () => console.log('Ctrl+- triggered')
71- // This will be handled by the browser's zoom controls
72- console.log('Ctrl+- triggered');
73- }
74 }100 }
75 ];101 ];
76 shortcutsLoading = false;102 shortcutsLoading = false;
@@ -100,6 +126,38 @@
100 window.location.href = url;126 window.location.href = url;
101 };127 };
102 128
129+ // Delete a history entry
130+ const deleteHistoryEntry = async (id: number, event: Event) => {
131+ event.stopPropagation(); // Prevent navigation
132+
133+ // Add to deleting set for animation
134+ deletingIds = new Set([...deletingIds, id]);
135+
136+ try {
137+ const response = await fetch(`dumb://homepage/api/history/delete?id=${id}`);
138+ if (response.ok) {
139+ // Wait a bit for the fade animation to complete
140+ await new Promise(resolve => setTimeout(resolve, 200));
141+
142+ // Refresh history after successful deletion
143+ await fetchHistory();
144+ await fetchStats();
145+ await fetchTopVisited();
146+ } else {
147+ console.error('Failed to delete history entry:', response.status);
148+ // Remove from deleting set if failed
149+ deletingIds = new Set([...deletingIds].filter(deletingId => deletingId !== id));
150+ }
151+ } catch (error) {
152+ console.error('Error deleting history entry:', error);
153+ // Remove from deleting set if failed
154+ deletingIds = new Set([...deletingIds].filter(deletingId => deletingId !== id));
155+ }
156+
157+ // Remove from deleting set after refresh
158+ deletingIds = new Set([...deletingIds].filter(deletingId => deletingId !== id));
159+ };
160+
103 // Get domain from URL161 // Get domain from URL
104 const getDomain = (url: string): string => {162 const getDomain = (url: string): string => {
105 try {163 try {
@@ -131,9 +189,49 @@
131 }189 }
132 };190 };
133 191
192+ // Fetch history stats from the API
193+ const fetchStats = async () => {
194+ try {
195+ const response = await fetch('dumb://homepage/api/history/stats');
196+ if (response.ok) {
197+ const data = await response.json();
198+ stats = data;
199+ } else {
200+ console.warn('Failed to fetch stats:', response.status);
201+ stats = null;
202+ }
203+ } catch (error) {
204+ console.error('Error fetching stats:', error);
205+ stats = null;
206+ } finally {
207+ statsLoading = false;
208+ }
209+ };
210+
211+ // Fetch top visited sites using search endpoint sorted by visit count
212+ const fetchTopVisited = async () => {
213+ try {
214+ const response = await fetch('dumb://homepage/api/history/search?q=&limit=5');
215+ if (response.ok) {
216+ const data = await response.json();
217+ topVisited = Array.isArray(data) ? data.slice(0, 5) : [];
218+ } else {
219+ console.warn('Failed to fetch top visited:', response.status);
220+ topVisited = [];
221+ }
222+ } catch (error) {
223+ console.error('Error fetching top visited:', error);
224+ topVisited = [];
225+ } finally {
226+ topVisitedLoading = false;
227+ }
228+ };
229+
134 onMount(() => {230 onMount(() => {
135 initializeShortcuts();231 initializeShortcuts();
136 fetchHistory();232 fetchHistory();
233+ fetchStats();
234+ fetchTopVisited();
137 });235 });
138 236
139 </script>237 </script>
@@ -154,6 +252,7 @@
154 {#each historyItems as item (item.url)}252 {#each historyItems as item (item.url)}
155 <div253 <div
156 class="history-item"254 class="history-item"
255+ class:deleting={deletingIds.has(item.id)}
157 role="button"256 role="button"
158 tabindex="0"257 tabindex="0"
159 onclick={() => navigateTo(item.url)}258 onclick={() => navigateTo(item.url)}
@@ -182,9 +281,13 @@
182 }281 }
183 }}282 }}
184 />283 />
185- <div class="history-favicon-fallback" style="display: none;">🌐</div>284+ <div class="history-favicon-fallback" style="display: none;">
285+ <Globe size={12} />
286+ </div>
186 {:else}287 {:else}
187- <div class="history-favicon-fallback">🌐</div>288+ <div class="history-favicon-fallback">
289+ <Globe size={12} />
290+ </div>
188 {/if}291 {/if}
189 </div>292 </div>
190 <div class="history-title">{item.title || 'Untitled'}</div>293 <div class="history-title">{item.title || 'Untitled'}</div>
@@ -193,6 +296,16 @@
193 <div class="history-sep">•</div>296 <div class="history-sep">•</div>
194 <div class="history-url">{item.url}</div>297 <div class="history-url">{item.url}</div>
195 <div class="history-time">{formatTime(item.last_visited)}</div>298 <div class="history-time">{formatTime(item.last_visited)}</div>
299+ <div
300+ class="history-delete"
301+ role="button"
302+ tabindex="0"
303+ onclick={(e) => deleteHistoryEntry(item.id, e)}
304+ onkeydown={(e) => e.key === 'Enter' && deleteHistoryEntry(item.id, e)}
305+ title="Delete this entry"
306+ >
307+ <Trash2 size={16} />
308+ </div>
196 </div>309 </div>
197 </div>310 </div>
198 {/each}311 {/each}
@@ -202,22 +315,68 @@
202 315
203 <div class="shortcuts-section">316 <div class="shortcuts-section">
204 <h2 class="section-title">Keyboard Shortcuts</h2>317 <h2 class="section-title">Keyboard Shortcuts</h2>
205- <div class="shortcuts-grid">318+ <div class="shortcuts-container">
206 {#if shortcutsLoading}319 {#if shortcutsLoading}
207 <div class="loading">Loading shortcuts...</div>320 <div class="loading">Loading shortcuts...</div>
208 {:else}321 {:else}
209- {#each shortcuts as shortcut (shortcut.key)}322+ <table class="shortcuts-table">
210- <div323+ <tbody>
211- class="shortcut"324+ {#each shortcuts as shortcut (shortcut.key)}
212- role="button"325+ <tr class="shortcut-row">
213- tabindex="0"326+ <td class="shortcut-key">{shortcut.key}</td>
214- onclick={shortcut.action}327+ <td class="shortcut-desc">{shortcut.description}</td>
215- onkeydown={(e) => e.key === 'Enter' && shortcut.action()}328+ </tr>
216- >329+ {/each}
217- <div class="shortcut-key">{shortcut.key}</div>330+ </tbody>
218- <div class="shortcut-desc">{shortcut.description}</div>331+ </table>
332+ {/if}
333+ </div>
334+
335+ <h2 class="section-title stats-title">Statistics</h2>
336+ <div class="stats-container">
337+ {#if statsLoading}
338+ <div class="loading">Loading stats...</div>
339+ {:else if stats}
340+ <div class="stats-grid">
341+ <div class="stat-item">
342+ <div class="stat-value">{stats.total_entries}</div>
343+ <div class="stat-label">History Entries</div>
219 </div>344 </div>
220- {/each}345+ </div>
346+ {:else}
347+ <div class="empty-state">
348+ <p>No statistics available</p>
349+ </div>
350+ {/if}
351+ </div>
352+
353+ <h2 class="section-title top-visited-title">Top 5 Most Visited</h2>
354+ <div class="top-visited-container">
355+ {#if topVisitedLoading}
356+ <div class="loading">Loading top visited...</div>
357+ {:else if topVisited.length === 0}
358+ <div class="empty-state">
359+ <p>No visit data available</p>
360+ </div>
361+ {:else}
362+ <div class="top-visited-list">
363+ {#each topVisited as item, index (item.url)}
364+ <div
365+ class="top-visited-item"
366+ role="button"
367+ tabindex="0"
368+ onclick={() => navigateTo(item.url)}
369+ onkeydown={(e) => e.key === 'Enter' && navigateTo(item.url)}
370+ >
371+ <div class="rank">{index + 1}</div>
372+ <div class="site-info">
373+ <div class="site-title">{item.title || 'Untitled'}</div>
374+ <div class="site-domain">{getDomain(item.url)}</div>
375+ </div>
376+ <div class="visit-count">{item.visit_count}</div>
377+ </div>
378+ {/each}
379+ </div>
221 {/if}380 {/if}
222 </div>381 </div>
223 </div>382 </div>
@@ -258,7 +417,7 @@
258 }417 }
259 418
260 .history-section {419 .history-section {
261- flex: 1;420+ flex: 2;
262 min-height: 0;421 min-height: 0;
263 min-width: 0;422 min-width: 0;
264 }423 }
@@ -290,10 +449,13 @@
290 background: #2d2d2d;449 background: #2d2d2d;
291 border-radius: 6px;450 border-radius: 6px;
292 cursor: pointer;451 cursor: pointer;
293- transition: background 0.2s;452+ transition: all 0.2s ease-in-out;
294 border-left: 3px solid #404040;453 border-left: 3px solid #404040;
295 overflow: hidden;454 overflow: hidden;
296 max-width: 100%;455 max-width: 100%;
456+ position: relative;
457+ opacity: 1;
458+ transform: translateX(0);
297 }459 }
298 460
299 .history-item:hover {461 .history-item:hover {
@@ -301,6 +463,17 @@
301 border-left-color: #0066cc;463 border-left-color: #0066cc;
302 }464 }
303 465
466+ .history-item:hover .history-delete {
467+ opacity: 1;
468+ visibility: visible;
469+ }
470+
471+ .history-item.deleting {
472+ opacity: 0;
473+ transform: translateX(-20px);
474+ pointer-events: none;
475+ }
476+
304 .history-line {477 .history-line {
305 display: flex;478 display: flex;
306 gap: 0.5rem;479 gap: 0.5rem;
@@ -333,6 +506,10 @@
333 506
334 .history-favicon-fallback {507 .history-favicon-fallback {
335 font-size: 12px;508 font-size: 12px;
509+ display: flex;
510+ align-items: center;
511+ justify-content: center;
512+ color: #888;
336 }513 }
337 514
338 .history-title {515 .history-title {
@@ -370,40 +547,176 @@
370 margin-left: auto;547 margin-left: auto;
371 }548 }
372 549
373- .shortcuts-grid {550+ .history-delete {
374- display: grid;551+ position: absolute;
375- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));552+ right: 0.75rem;
376- gap: 1rem;553+ top: 50%;
554+ transform: translateY(-50%);
555+ opacity: 0;
556+ visibility: hidden;
557+ transition: opacity 0.2s, visibility 0.2s, transform 0.2s;
558+ cursor: pointer;
559+ background: #1a1a1a;
560+ border-radius: 3px;
561+ padding: 0.25rem;
562+ border: 1px solid #404040;
563+ display: flex;
564+ align-items: center;
565+ justify-content: center;
566+ width: 2rem;
567+ height: 2rem;
568+ color: #888;
569+ }
570+
571+ .history-delete:hover {
572+ background: #ff4444;
573+ border-color: #ff6666;
574+ color: #fff;
575+ transform: translateY(-50%) scale(1.1);
576+ }
577+
578+ .shortcuts-container {
377 max-height: calc(100vh - 8rem);579 max-height: calc(100vh - 8rem);
378 overflow-y: auto;580 overflow-y: auto;
379 }581 }
380 582
381- .shortcut {583+ .shortcuts-table {
584+ width: 100%;
585+ border-collapse: collapse;
586+ background: #2d2d2d;
587+ border-radius: 6px;
588+ overflow: hidden;
589+ }
590+
591+ .shortcut-row {
592+ border-bottom: 1px solid #404040;
593+ transition: background 0.2s;
594+ }
595+
596+ .shortcut-row:last-child {
597+ border-bottom: none;
598+ }
599+
600+ .shortcut-row:hover {
601+ background: #3d3d3d;
602+ }
603+
604+ .shortcut-key {
605+ padding: 0.4rem 0.8rem;
606+ font-weight: bold;
607+ color: #0066cc;
608+ font-size: 0.85rem;
609+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
610+ white-space: nowrap;
611+ width: 1%;
612+ }
613+
614+ .shortcut-desc {
615+ padding: 0.4rem 0.8rem;
616+ color: #e6e6e6;
617+ font-size: 0.85rem;
618+ }
619+
620+ .stats-title {
621+ margin-top: 2rem;
622+ }
623+
624+ .stats-container {
625+ margin-top: 1rem;
626+ }
627+
628+ .stats-grid {
629+ display: flex;
630+ gap: 1rem;
631+ }
632+
633+ .stat-item {
634+ background: #2d2d2d;
635+ border-radius: 6px;
382 padding: 1rem;636 padding: 1rem;
637+ text-align: center;
638+ flex: 1;
639+ border: 1px solid #404040;
640+ }
641+
642+ .stat-value {
643+ font-size: 1.5rem;
644+ font-weight: bold;
645+ color: #0066cc;
646+ margin-bottom: 0.25rem;
647+ }
648+
649+ .stat-label {
650+ font-size: 0.8rem;
651+ color: #888;
652+ text-transform: uppercase;
653+ letter-spacing: 0.5px;
654+ }
655+
656+ .top-visited-title {
657+ margin-top: 2rem;
658+ }
659+
660+ .top-visited-container {
661+ margin-top: 1rem;
662+ }
663+
664+ .top-visited-list {
665+ display: flex;
666+ flex-direction: column;
667+ gap: 0.5rem;
668+ }
669+
670+ .top-visited-item {
671+ display: flex;
672+ align-items: center;
673+ padding: 0.75rem;
383 background: #2d2d2d;674 background: #2d2d2d;
384- border: 2px solid #404040;675+ border-radius: 6px;
385- border-radius: 8px;
386 cursor: pointer;676 cursor: pointer;
387- transition: all 0.2s;677+ transition: background 0.2s;
388- text-align: center;678+ border: 1px solid #404040;
679+ gap: 1rem;
389 }680 }
390 681
391- .shortcut:hover {682+ .top-visited-item:hover {
392 background: #3d3d3d;683 background: #3d3d3d;
393 border-color: #0066cc;684 border-color: #0066cc;
394- transform: translateY(-2px);
395 }685 }
396 686
397- .shortcut-key {687+ .rank {
398 font-weight: bold;688 font-weight: bold;
399 color: #0066cc;689 color: #0066cc;
400- margin-bottom: 0.5rem;
401 font-size: 1.1rem;690 font-size: 1.1rem;
691+ min-width: 1.5rem;
692+ text-align: center;
402 }693 }
403 694
404- .shortcut-desc {695+ .site-info {
405- color: #888;696+ flex: 1;
697+ min-width: 0;
698+ }
699+
700+ .site-title {
406 font-size: 0.9rem;701 font-size: 0.9rem;
702+ color: #e6e6e6;
703+ margin-bottom: 0.2rem;
704+ white-space: nowrap;
705+ overflow: hidden;
706+ text-overflow: ellipsis;
707+ }
708+
709+ .site-domain {
710+ font-size: 0.8rem;
711+ color: #888;
712+ }
713+
714+ .visit-count {
715+ font-size: 0.85rem;
716+ color: #0066cc;
717+ font-weight: bold;
718+ min-width: 2rem;
719+ text-align: center;
407 }720 }
408 721
409 .loading {722 .loading {
@@ -428,9 +741,5 @@
428 flex-direction: column;741 flex-direction: column;
429 padding: 1rem;742 padding: 1rem;
430 }743 }
431-
432- .shortcuts-grid {
433- grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
434- }
435 }744 }
436 </style>⏎̸745 </style>⏎̸
M internal/app/api/routes.go +20 -0
@@ -107,6 +107,26 @@ func (h *Handler) HandleHistoryStats() (string, []byte, bool) {
107107 return constants.ContentTypeJSON, b, true
108108 }
109109
110+// HandleHistoryDelete handles GET /history/delete endpoint
111+func (h *Handler) HandleHistoryDelete(u *neturl.URL) (string, []byte, bool) {
112+ log.Printf("[api] GET /history/delete%s", u.RawQuery)
113+ q := u.Query()
114+ idStr := q.Get("id")
115+ if idStr == "" {
116+ return constants.ContentTypeJSON, []byte(`{"error":"id parameter required"}`), true
117+ }
118+ id, err := strconv.ParseInt(idStr, 10, 64)
119+ if err != nil {
120+ return constants.ContentTypeJSON, []byte(`{"error":"invalid id parameter"}`), true
121+ }
122+ ctx := context.Background()
123+ err = h.browserService.DeleteHistoryEntry(ctx, id)
124+ if err != nil {
125+ return constants.ContentTypeJSON, []byte(`{"error":"failed to delete entry"}`), true
126+ }
127+ return constants.ContentTypeJSON, []byte(`{"success":true}`), true
128+}
129+
110130 // HandleDefault returns empty JSON for unknown endpoints
111131 func (h *Handler) HandleDefault() (string, []byte, bool) {
112132 return constants.ContentTypeJSON, []byte("{}"), true
@@ -107,6 +107,26 @@ func (h *Handler) HandleHistoryStats() (string, []byte, bool) {
107 return constants.ContentTypeJSON, b, true107 return constants.ContentTypeJSON, b, true
108 }108 }
109 109
110+// HandleHistoryDelete handles GET /history/delete endpoint
111+func (h *Handler) HandleHistoryDelete(u *neturl.URL) (string, []byte, bool) {
112+ log.Printf("[api] GET /history/delete%s", u.RawQuery)
113+ q := u.Query()
114+ idStr := q.Get("id")
115+ if idStr == "" {
116+ return constants.ContentTypeJSON, []byte(`{"error":"id parameter required"}`), true
117+ }
118+ id, err := strconv.ParseInt(idStr, 10, 64)
119+ if err != nil {
120+ return constants.ContentTypeJSON, []byte(`{"error":"invalid id parameter"}`), true
121+ }
122+ ctx := context.Background()
123+ err = h.browserService.DeleteHistoryEntry(ctx, id)
124+ if err != nil {
125+ return constants.ContentTypeJSON, []byte(`{"error":"failed to delete entry"}`), true
126+ }
127+ return constants.ContentTypeJSON, []byte(`{"success":true}`), true
128+}
129+
110 // HandleDefault returns empty JSON for unknown endpoints130 // HandleDefault returns empty JSON for unknown endpoints
111 func (h *Handler) HandleDefault() (string, []byte, bool) {131 func (h *Handler) HandleDefault() (string, []byte, bool) {
112 return constants.ContentTypeJSON, []byte("{}"), true132 return constants.ContentTypeJSON, []byte("{}"), true
M internal/app/api/scheme.go +2 -0
@@ -85,6 +85,8 @@ func (s *SchemeHandler) handleAPIRoute(u *neturl.URL, cfg *config.Config) (strin
8585 return s.apiHandler.HandleHistorySearch(u)
8686 case strings.HasPrefix(path, "/history/stats"):
8787 return s.apiHandler.HandleHistoryStats()
88+ case strings.HasPrefix(path, "/history/delete"):
89+ return s.apiHandler.HandleHistoryDelete(u)
8890 default:
8991 return s.apiHandler.HandleDefault()
9092 }
@@ -85,6 +85,8 @@ func (s *SchemeHandler) handleAPIRoute(u *neturl.URL, cfg *config.Config) (strin
85 return s.apiHandler.HandleHistorySearch(u)85 return s.apiHandler.HandleHistorySearch(u)
86 case strings.HasPrefix(path, "/history/stats"):86 case strings.HasPrefix(path, "/history/stats"):
87 return s.apiHandler.HandleHistoryStats()87 return s.apiHandler.HandleHistoryStats()
88+ case strings.HasPrefix(path, "/history/delete"):
89+ return s.apiHandler.HandleHistoryDelete(u)
88 default:90 default:
89 return s.apiHandler.HandleDefault()91 return s.apiHandler.HandleDefault()
90 }92 }
M internal/db/history.sql.go +10 -0
@@ -25,6 +25,16 @@ func (q *Queries) AddOrUpdateHistory(ctx context.Context, url string, title sql.
2525 return err
2626 }
2727
28+const DeleteHistory = `-- name: DeleteHistory :exec
29+DELETE FROM history
30+WHERE id = ?
31+`
32+
33+func (q *Queries) DeleteHistory(ctx context.Context, id int64) error {
34+ _, err := q.db.ExecContext(ctx, DeleteHistory, id)
35+ return err
36+}
37+
2838 const GetHistory = `-- name: GetHistory :many
2939 SELECT id, url, title, visit_count, last_visited, created_at, favicon_url
3040 FROM history
@@ -25,6 +25,16 @@ func (q *Queries) AddOrUpdateHistory(ctx context.Context, url string, title sql.
25 return err25 return err
26 }26 }
27 27
28+const DeleteHistory = `-- name: DeleteHistory :exec
29+DELETE FROM history
30+WHERE id = ?
31+`
32+
33+func (q *Queries) DeleteHistory(ctx context.Context, id int64) error {
34+ _, err := q.db.ExecContext(ctx, DeleteHistory, id)
35+ return err
36+}
37+
28 const GetHistory = `-- name: GetHistory :many38 const GetHistory = `-- name: GetHistory :many
29 SELECT id, url, title, visit_count, last_visited, created_at, favicon_url39 SELECT id, url, title, visit_count, last_visited, created_at, favicon_url
30 FROM history40 FROM history
M internal/db/interfaces.go +1 -0
@@ -20,6 +20,7 @@ type HistoryQuerier interface {
2020 SearchHistory(ctx context.Context, column1 sql.NullString, column2 sql.NullString, limit int64) ([]History, error)
2121 AddOrUpdateHistory(ctx context.Context, url string, title sql.NullString) error
2222 UpdateHistoryFavicon(ctx context.Context, faviconUrl sql.NullString, url string) error
23+ DeleteHistory(ctx context.Context, id int64) error
2324 }
2425
2526 // ShortcutQuerier defines the interface for shortcut-related database operations
@@ -20,6 +20,7 @@ type HistoryQuerier interface {
20 SearchHistory(ctx context.Context, column1 sql.NullString, column2 sql.NullString, limit int64) ([]History, error)20 SearchHistory(ctx context.Context, column1 sql.NullString, column2 sql.NullString, limit int64) ([]History, error)
21 AddOrUpdateHistory(ctx context.Context, url string, title sql.NullString) error21 AddOrUpdateHistory(ctx context.Context, url string, title sql.NullString) error
22 UpdateHistoryFavicon(ctx context.Context, faviconUrl sql.NullString, url string) error22 UpdateHistoryFavicon(ctx context.Context, faviconUrl sql.NullString, url string) error
23+ DeleteHistory(ctx context.Context, id int64) error
23 }24 }
24 25
25 // ShortcutQuerier defines the interface for shortcut-related database operations26 // ShortcutQuerier defines the interface for shortcut-related database operations
M internal/db/querier.go +1 -0
@@ -15,6 +15,7 @@ type Querier interface {
1515 CleanupOldZoomLevels(ctx context.Context, dollar_1 sql.NullString) error
1616 DeleteCertificateValidation(ctx context.Context, hostname string, certificateHash string) error
1717 DeleteExpiredCertificateValidations(ctx context.Context) error
18+ DeleteHistory(ctx context.Context, id int64) error
1819 // Delete zoom level setting for a domain
1920 DeleteZoomLevel(ctx context.Context, domain string) error
2021 GetCertificateValidation(ctx context.Context, hostname string, certificateHash string) (CertificateValidation, error)
@@ -15,6 +15,7 @@ type Querier interface {
15 CleanupOldZoomLevels(ctx context.Context, dollar_1 sql.NullString) error15 CleanupOldZoomLevels(ctx context.Context, dollar_1 sql.NullString) error
16 DeleteCertificateValidation(ctx context.Context, hostname string, certificateHash string) error16 DeleteCertificateValidation(ctx context.Context, hostname string, certificateHash string) error
17 DeleteExpiredCertificateValidations(ctx context.Context) error17 DeleteExpiredCertificateValidations(ctx context.Context) error
18+ DeleteHistory(ctx context.Context, id int64) error
18 // Delete zoom level setting for a domain19 // Delete zoom level setting for a domain
19 DeleteZoomLevel(ctx context.Context, domain string) error20 DeleteZoomLevel(ctx context.Context, domain string) error
20 GetCertificateValidation(ctx context.Context, hostname string, certificateHash string) (CertificateValidation, error)21 GetCertificateValidation(ctx context.Context, hostname string, certificateHash string) (CertificateValidation, error)
M internal/db/queries/history.sql +4 -0
@@ -25,6 +25,10 @@ UPDATE history
2525 SET favicon_url = ?
2626 WHERE url = ?;
2727
28+-- name: DeleteHistory :exec
29+DELETE FROM history
30+WHERE id = ?;
31+
2832 -- name: GetShortcuts :many
2933 SELECT id, shortcut, url_template, description, created_at
3034 FROM shortcuts
@@ -25,6 +25,10 @@ UPDATE history
25 SET favicon_url = ?25 SET favicon_url = ?
26 WHERE url = ?;26 WHERE url = ?;
27 27
28+-- name: DeleteHistory :exec
29+DELETE FROM history
30+WHERE id = ?;
31+
28 -- name: GetShortcuts :many32 -- name: GetShortcuts :many
29 SELECT id, shortcut, url_template, description, created_at33 SELECT id, shortcut, url_template, description, created_at
30 FROM shortcuts34 FROM shortcuts
M services/browser_service.go +1 -5
@@ -287,11 +287,7 @@ func (s *BrowserService) SearchHistory(ctx context.Context, query string, limit
287287
288288 // DeleteHistoryEntry removes a specific history entry.
289289 func (s *BrowserService) DeleteHistoryEntry(ctx context.Context, id int64) error {
290- _ = ctx
291- _ = id
292- // Note: DeleteHistory method doesn't exist in current schema
293- // This would need to be implemented in the database layer
294- return fmt.Errorf("delete history not implemented yet")
290+ return s.dbQueries.DeleteHistory(ctx, id)
295291 }
296292
297293 // ClearHistory removes all history entries.
@@ -287,11 +287,7 @@ func (s *BrowserService) SearchHistory(ctx context.Context, query string, limit
287 287
288 // DeleteHistoryEntry removes a specific history entry.288 // DeleteHistoryEntry removes a specific history entry.
289 func (s *BrowserService) DeleteHistoryEntry(ctx context.Context, id int64) error {289 func (s *BrowserService) DeleteHistoryEntry(ctx context.Context, id int64) error {
290- _ = ctx290+ return s.dbQueries.DeleteHistory(ctx, id)
291- _ = id
292- // Note: DeleteHistory method doesn't exist in current schema
293- // This would need to be implemented in the database layer
294- return fmt.Errorf("delete history not implemented yet")
295 }291 }
296 292
297 // ClearHistory removes all history entries.293 // ClearHistory removes all history entries.

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