HistoryPanel.svelte RawHistoryFolder
1<script lang="ts">
2 import { homepageState } from '../state.svelte';
3 import {
4 fetchHistoryTimeline,
5 deleteHistoryEntry,
6 deleteHistoryByRange,
7 clearAllHistory,
8 fetchDomainStats,
9 navigateTo
10 } from '../messaging';
11 import type { HistoryEntry, HistoryCleanupRange } from '../types';
12
13 import { HistoryTimeline, HistorySearch, HistoryFilters, HistoryCleanup } from '../history';
14
15 let cleanupOpen = $state(false);
16 let selectedDomain = $state<string | null>(null);
17
18 // Filter entries by domain if selected
19 const displayedEntries = $derived(() => {
20 if (!selectedDomain) {
21 return homepageState.historySearchQuery
22 ? homepageState.historySearchResults
23 : homepageState.history;
24 }
25
26 const entries = homepageState.historySearchQuery
27 ? homepageState.historySearchResults
28 : homepageState.history;
29
30 return entries.filter(entry => {
31 try {
32 return new URL(entry.url).hostname === selectedDomain;
33 } catch {
34 return false;
35 }
36 });
37 });
38
39 // Recompute timeline from filtered entries
40 const displayedTimeline = $derived(() => {
41 const entries = displayedEntries();
42 if (entries.length === 0) return [];
43
44 const groups = new Map<string, HistoryEntry[]>();
45 const today = new Date();
46 today.setHours(0, 0, 0, 0);
47 const yesterday = new Date(today);
48 yesterday.setDate(yesterday.getDate() - 1);
49
50 for (const entry of entries) {
51 const date = new Date(entry.last_visited);
52 const dateKey = date.toISOString().split('T')[0] ?? '';
53 if (!groups.has(dateKey)) {
54 groups.set(dateKey, []);
55 }
56 groups.get(dateKey)!.push(entry);
57 }
58
59 const result: { date: string; label: string; entries: HistoryEntry[] }[] = [];
60 for (const [dateKey, items] of groups) {
61 const date = new Date(dateKey);
62 let label: string;
63
64 if (date >= today) {
65 label = 'Today';
66 } else if (date >= yesterday) {
67 label = 'Yesterday';
68 } else {
69 label = date.toLocaleDateString(undefined, {
70 weekday: 'short',
71 month: 'short',
72 day: 'numeric',
73 });
74 }
75
76 result.push({ date: dateKey, label, entries: items });
77 }
78
79 return result.sort((a, b) => b.date.localeCompare(a.date));
80 });
81
82 const handleSelectEntry = (entry: HistoryEntry) => {
83 navigateTo(entry.url);
84 };
85
86 const handleDeleteEntry = async (entry: HistoryEntry) => {
87 await deleteHistoryEntry(entry.id);
88 };
89
90 const handleLoadMore = async () => {
91 if (homepageState.hasMoreHistory && !homepageState.historyLoading) {
92 await fetchHistoryTimeline(50, homepageState.historyOffset);
93 }
94 };
95
96 const handleCleanup = async (range: HistoryCleanupRange) => {
97 if (range === 'all') {
98 await clearAllHistory();
99 } else {
100 await deleteHistoryByRange(range);
101 }
102 cleanupOpen = false;
103 };
104
105 const handleSelectDomain = (domain: string | null) => {
106 selectedDomain = domain;
107 homepageState.setFocusedIndex(0);
108 };
109
110 // Fetch domain stats on mount
111 $effect(() => {
112 fetchDomainStats(10);
113 });
114</script>
115
116<div class="history-panel">
117 <div class="panel-toolbar">
118 <HistorySearch placeholder="Search history (FTS)..." autofocus={true} />
119 </div>
120
121 <HistoryFilters
122 domains={homepageState.domainStats}
123 selectedDomain={selectedDomain}
124 onSelectDomain={handleSelectDomain}
125 onOpenCleanup={() => cleanupOpen = true}
126 />
127
128 <div class="panel-content">
129 {#if homepageState.historyLoading && homepageState.history.length === 0}
130 <div class="loading-state">
131 <span class="loading-spinner"></span>
132 <span class="loading-text">LOADING HISTORY...</span>
133 </div>
134 {:else}
135 <HistoryTimeline
136 groups={displayedTimeline()}
137 onSelectEntry={handleSelectEntry}
138 onDeleteEntry={handleDeleteEntry}
139 onLoadMore={handleLoadMore}
140 />
141 {/if}
142 </div>
143
144 {#if cleanupOpen}
145 <HistoryCleanup
146 onCleanup={handleCleanup}
147 onClose={() => cleanupOpen = false}
148 />
149 {/if}
150</div>
151
152<style>
153 .history-panel {
154 display: flex;
155 flex-direction: column;
156 gap: 0.5rem;
157 padding: 0.5rem 1rem;
158 }
159
160 .panel-toolbar {
161 flex-shrink: 0;
162 }
163
164 .panel-content {
165 display: flex;
166 flex-direction: column;
167 border-width: 1px;
168 border-style: solid;
169 border-color: var(--border);
170 background: color-mix(in srgb, var(--background) 95%, var(--card) 5%);
171 }
172
173 .loading-state {
174 flex: 1;
175 display: flex;
176 flex-direction: column;
177 align-items: center;
178 justify-content: center;
179 gap: 0.75rem;
180 color: var(--muted-foreground);
181 }
182
183 .loading-spinner {
184 width: 24px;
185 height: 24px;
186 border-width: 2px;
187 border-style: solid;
188 border-color: var(--border);
189 border-top-color: var(--primary, #4ade80);
190 border-radius: 50%;
191 animation: spin 0.8s linear infinite;
192 }
193
194 @keyframes spin {
195 to { transform: rotate(360deg); }
196 }
197
198 .loading-text {
199 font-size: 0.68rem;
200 letter-spacing: 0.12em;
201 animation: pulse 1.5s ease-in-out infinite;
202 }
203
204 @keyframes pulse {
205 0%, 100% { opacity: 0.5; }
206 50% { opacity: 1; }
207 }
208</style>