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