v0.26.1 75cd777

ErrorPage.svelte RawHistoryFolder

1<script lang="ts">
2  import { onMount } from 'svelte';
3  import { ModeWatcher } from 'mode-watcher';
4  import { Button } from '$lib/components/ui/button';
5  import * as Card from '$lib/components/ui/card';
6
7  type ErrorConfig = {
8    code: number;
9    title: string;
10    message: string;
11    icon: string;
12    actions: string[];
13  };
14
15  const defaultConfig: ErrorConfig = {
16    code: 404,
17    title: 'Page Not Found',
18    message: 'The page you are looking for does not exist.',
19    icon: 'warning',
20    actions: ['back'],
21  };
22
23  // Error configurations
24  const errorConfigs: Record<string, ErrorConfig> = {
25    blocked: { code: 0, title: 'Page Blocked', message: 'Content blocked by filter', icon: 'shield', actions: ['whitelist', 'bypass', 'back'] },
26    '404': defaultConfig,
27    '500': { code: 500, title: 'Server Error', message: 'Something went wrong on the server.', icon: 'error', actions: ['retry', 'back'] },
28    '502': { code: 502, title: 'Bad Gateway', message: 'The server received an invalid response.', icon: 'error', actions: ['retry', 'back'] },
29    '503': { code: 503, title: 'Service Unavailable', message: 'The server is temporarily unavailable.', icon: 'error', actions: ['retry', 'back'] },
30    offline: { code: 0, title: 'You Are Offline', message: 'Check your internet connection.', icon: 'offline', actions: ['retry', 'back'] },
31    ssl: { code: 0, title: 'Connection Not Secure', message: 'The security certificate is invalid.', icon: 'lock', actions: ['bypass', 'back'] },
32    timeout: { code: 0, title: 'Connection Timed Out', message: 'The server took too long to respond.', icon: 'warning', actions: ['retry', 'back'] },
33  };
34
35  let config = $state<ErrorConfig>(defaultConfig);
36  let targetUrl = $state('');
37  let targetDomain = $state('');
38  let customMessage = $state('');
39  let isLoading = $state(false);
40  let actionError = $state('');
41
42  onMount(() => {
43    const params = new URLSearchParams(window.location.search);
44    const typeParam = params.get('type') || params.get('code') || '404';
45
46    const selectedConfig = errorConfigs[typeParam];
47    config = selectedConfig ?? {
48      code: parseInt(typeParam) || 0,
49      title: `Error ${typeParam}`,
50      message: 'An unexpected error occurred.',
51      icon: 'error',
52      actions: ['retry', 'back'],
53    };
54
55    targetUrl = params.get('url') || '';
56    customMessage = params.get('reason') || params.get('message') || '';
57
58    if (targetUrl) {
59      try {
60        targetDomain = new URL(targetUrl).hostname;
61      } catch {
62        targetDomain = targetUrl;
63      }
64    }
65  });
66
67  function sendMessage(type: string, payload: Record<string, unknown>): Promise<unknown> {
68    return new Promise((resolve, reject) => {
69      const requestId = `error_${Date.now()}_${Math.random().toString(36).slice(2)}`;
70      const handler = (event: MessageEvent) => {
71        if (event.data?.requestId === requestId) {
72          window.removeEventListener('message', handler);
73          if (event.data.success) {
74            resolve(event.data.data);
75          } else {
76            reject(new Error(event.data.error || 'Unknown error'));
77          }
78        }
79      };
80      window.addEventListener('message', handler);
81      if (window.webkit?.messageHandlers?.dumber) {
82        window.webkit.messageHandlers.dumber.postMessage(JSON.stringify({ type, requestId, payload }));
83      } else {
84        reject(new Error('WebKit message handler not available'));
85      }
86      setTimeout(() => { window.removeEventListener('message', handler); reject(new Error('Timeout')); }, 10000);
87    });
88  }
89
90  async function addToWhitelist() {
91    if (!targetDomain) return;
92    isLoading = true;
93    actionError = '';
94    try {
95      await sendMessage('addToWhitelist', { domain: targetDomain });
96      window.location.href = targetUrl;
97    } catch (e) {
98      actionError = e instanceof Error ? e.message : 'Failed';
99      isLoading = false;
100    }
101  }
102
103  async function bypassOnce() {
104    if (!targetUrl) return;
105    isLoading = true;
106    actionError = '';
107    try {
108      await sendMessage('bypassOnce', { url: targetUrl });
109      window.location.href = targetUrl;
110    } catch (e) {
111      actionError = e instanceof Error ? e.message : 'Failed';
112      isLoading = false;
113    }
114  }
115
116  function retry() {
117    if (targetUrl) {
118      window.location.href = targetUrl;
119    } else {
120      window.location.reload();
121    }
122  }
123
124  function goBack() {
125    window.history.back();
126  }
127
128  const icons: Record<string, string> = {
129    shield: '<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><path d="M9 12l2 2 4-4"/>',
130    warning: '<path d="M12 9v4"/><path d="M12 17h.01"/><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>',
131    error: '<circle cx="12" cy="12" r="10"/><path d="M15 9l-6 6"/><path d="M9 9l6 6"/>',
132    offline: '<line x1="1" y1="1" x2="23" y2="23"/><path d="M16.72 11.06A10.94 10.94 0 0 1 19 12.55"/><path d="M5 12.55a10.94 10.94 0 0 1 5.17-2.39"/><path d="M10.71 5.05A16 16 0 0 1 22.58 9"/><path d="M1.42 9a15.91 15.91 0 0 1 4.7-2.88"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><line x1="12" y1="20" x2="12.01" y2="20"/>',
133    lock: '<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>',
134  };
135
136  const iconColors: Record<string, string> = {
137    shield: 'text-primary',
138    warning: 'text-yellow-500',
139    error: 'text-destructive',
140    offline: 'text-muted-foreground',
141    lock: 'text-yellow-500',
142  };
143</script>
144
145<svelte:head>
146  <title>{config.title}</title>
147</svelte:head>
148
149<ModeWatcher />
150<div class="min-h-screen flex items-center justify-center p-8 bg-background">
151  <Card.Root class="w-full max-w-md">
152    <Card.Header class="text-center">
153      <div class="flex justify-center mb-4">
154        <svg class="w-16 h-16 {iconColors[config.icon] || 'text-muted-foreground'}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
155          <!-- eslint-disable-next-line svelte/no-at-html-tags -- trusted static SVG paths -->
156          {@html icons[config.icon] || icons.error}
157        </svg>
158      </div>
159      {#if config.code > 0}
160        <div class="text-6xl font-bold text-muted-foreground/30 mb-2">{config.code}</div>
161      {/if}
162      <Card.Title class="text-xl">{config.title}</Card.Title>
163      <Card.Description>{customMessage || config.message}</Card.Description>
164    </Card.Header>
165
166    <Card.Content class="space-y-4">
167      {#if targetUrl}
168        <div class="bg-muted p-3 rounded-md">
169          <p class="text-xs text-muted-foreground uppercase tracking-wide mb-1">URL</p>
170          <code class="text-sm break-all">{targetUrl}</code>
171        </div>
172      {/if}
173
174      {#if actionError}
175        <div class="bg-destructive/10 border border-destructive/30 text-destructive p-3 rounded-md text-sm">
176          {actionError}
177        </div>
178      {/if}
179    </Card.Content>
180
181    <Card.Footer class="flex flex-col gap-3">
182      {#if config.actions.includes('whitelist') && targetDomain}
183        <Button class="w-full" onclick={addToWhitelist} disabled={isLoading}>
184          {isLoading ? 'Adding...' : 'Add to Whitelist'}
185        </Button>
186      {/if}
187
188      {#if config.actions.includes('bypass') && targetUrl}
189        <Button variant="outline" class="w-full" onclick={bypassOnce} disabled={isLoading}>
190          Proceed Anyway
191        </Button>
192      {/if}
193
194      {#if config.actions.includes('retry')}
195        <Button class="w-full" onclick={retry} disabled={isLoading}>
196          Try Again
197        </Button>
198      {/if}
199
200      {#if config.actions.includes('back')}
201        <Button variant="ghost" class="w-full" onclick={goBack} disabled={isLoading}>
202          Go Back
203        </Button>
204      {/if}
205
206      {#if config.actions.includes('whitelist') && targetDomain}
207        <p class="text-xs text-muted-foreground text-center">
208          Adding <strong class="text-foreground">{targetDomain}</strong> to whitelist will disable content filtering.
209        </p>
210      {/if}
211    </Card.Footer>
212  </Card.Root>
213</div>

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