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 event.data.success ? resolve(event.data.data) : reject(new Error(event.data.error || 'Unknown error'));
74 }
75 };
76 window.addEventListener('message', handler);
77 if (window.webkit?.messageHandlers?.dumber) {
78 window.webkit.messageHandlers.dumber.postMessage(JSON.stringify({ type, requestId, payload }));
79 } else {
80 reject(new Error('WebKit message handler not available'));
81 }
82 setTimeout(() => { window.removeEventListener('message', handler); reject(new Error('Timeout')); }, 10000);
83 });
84 }
85
86 async function addToWhitelist() {
87 if (!targetDomain) return;
88 isLoading = true;
89 actionError = '';
90 try {
91 await sendMessage('addToWhitelist', { domain: targetDomain });
92 window.location.href = targetUrl;
93 } catch (e) {
94 actionError = e instanceof Error ? e.message : 'Failed';
95 isLoading = false;
96 }
97 }
98
99 async function bypassOnce() {
100 if (!targetUrl) return;
101 isLoading = true;
102 actionError = '';
103 try {
104 await sendMessage('bypassOnce', { url: targetUrl });
105 window.location.href = targetUrl;
106 } catch (e) {
107 actionError = e instanceof Error ? e.message : 'Failed';
108 isLoading = false;
109 }
110 }
111
112 function retry() {
113 targetUrl ? window.location.href = targetUrl : window.location.reload();
114 }
115
116 function goBack() {
117 window.history.back();
118 }
119
120 const icons: Record<string, string> = {
121 shield: '<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><path d="M9 12l2 2 4-4"/>',
122 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"/>',
123 error: '<circle cx="12" cy="12" r="10"/><path d="M15 9l-6 6"/><path d="M9 9l6 6"/>',
124 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"/>',
125 lock: '<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>',
126 };
127
128 const iconColors: Record<string, string> = {
129 shield: 'text-primary',
130 warning: 'text-yellow-500',
131 error: 'text-destructive',
132 offline: 'text-muted-foreground',
133 lock: 'text-yellow-500',
134 };
135</script>
136
137<svelte:head>
138 <title>{config.title}</title>
139</svelte:head>
140
141<ModeWatcher />
142<div class="min-h-screen flex items-center justify-center p-8 bg-background">
143 <Card.Root class="w-full max-w-md">
144 <Card.Header class="text-center">
145 <div class="flex justify-center mb-4">
146 <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">
147 {@html icons[config.icon] || icons.error}
148 </svg>
149 </div>
150 {#if config.code > 0}
151 <div class="text-6xl font-bold text-muted-foreground/30 mb-2">{config.code}</div>
152 {/if}
153 <Card.Title class="text-xl">{config.title}</Card.Title>
154 <Card.Description>{customMessage || config.message}</Card.Description>
155 </Card.Header>
156
157 <Card.Content class="space-y-4">
158 {#if targetUrl}
159 <div class="bg-muted p-3 rounded-md">
160 <p class="text-xs text-muted-foreground uppercase tracking-wide mb-1">URL</p>
161 <code class="text-sm break-all">{targetUrl}</code>
162 </div>
163 {/if}
164
165 {#if actionError}
166 <div class="bg-destructive/10 border border-destructive/30 text-destructive p-3 rounded-md text-sm">
167 {actionError}
168 </div>
169 {/if}
170 </Card.Content>
171
172 <Card.Footer class="flex flex-col gap-3">
173 {#if config.actions.includes('whitelist') && targetDomain}
174 <Button class="w-full" onclick={addToWhitelist} disabled={isLoading}>
175 {isLoading ? 'Adding...' : 'Add to Whitelist'}
176 </Button>
177 {/if}
178
179 {#if config.actions.includes('bypass') && targetUrl}
180 <Button variant="outline" class="w-full" onclick={bypassOnce} disabled={isLoading}>
181 Proceed Anyway
182 </Button>
183 {/if}
184
185 {#if config.actions.includes('retry')}
186 <Button class="w-full" onclick={retry} disabled={isLoading}>
187 Try Again
188 </Button>
189 {/if}
190
191 {#if config.actions.includes('back')}
192 <Button variant="ghost" class="w-full" onclick={goBack} disabled={isLoading}>
193 Go Back
194 </Button>
195 {/if}
196
197 {#if config.actions.includes('whitelist') && targetDomain}
198 <p class="text-xs text-muted-foreground text-center">
199 Adding <strong class="text-foreground">{targetDomain}</strong> to whitelist will disable content filtering.
200 </p>
201 {/if}
202 </Card.Footer>
203 </Card.Root>
204</div>