v0.23.2 bfd5eb4

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>

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