shared.ts
Raw
1export function humanFileSize(size: number) {
2 if (size < 1024) return `${size} B`;
3 const i = Math.min(4, Math.floor(Math.log(size) / Math.log(1024)));
4 return `${(size / 1024 ** i).toFixed(2)} ${["B", "KiB", "MiB", "GiB", "TiB"][i]}`;
5}
6
7// TextDecoder-based UTF-8 check that works in both Bun and the browser.
8export function isValidUTF8(buf: ArrayBuffer | ArrayBufferView): boolean {
9 try {
10 new TextDecoder("utf-8", { fatal: true }).decode(buf);
11 return true;
12 } catch (_e) {
13 return false;
14 }
15}
16
17export type Theme = "auto" | "light" | "dark";
18
19// Reads the persisted theme choice from a raw Cookie request header, defaulting
20// to "auto" (follow the OS) when unset or unrecognized. Values are plain
21// (auto/light/dark), so no decoding is needed.
22export function getTheme(cookieHeader: string | undefined): Theme {
23 const value = cookieHeader?.match(/(?:^|;\s*)zbin-theme=([^;]+)/)?.[1];
24 return value === "light" || value === "dark" ? value : "auto";
25}
26
27// Inline SVG for the toggle's current state, drawn in `currentColor` so it picks
28// up the button's text color. Shared by the server (initial render) and
29// client-theme.js (on toggle): light = sun, dark = crescent, auto = a half-filled
30// disc suggesting "follows the system". Injected as raw markup (not escaped).
31export function themeIcon(theme: Theme): string {
32 const open =
33 '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">';
34 if (theme === "light") {
35 return `${open}<circle cx="12" cy="12" r="4.5"/><path d="M12 1.5v3M12 19.5v3M4.22 4.22l2.12 2.12M17.66 17.66l2.12 2.12M1.5 12h3M19.5 12h3M4.22 19.78l2.12-2.12M17.66 6.34l2.12-2.12"/></svg>`;
36 }
37 if (theme === "dark") {
38 return `${open}<path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg>`;
39 }
40 return `${open}<circle cx="12" cy="12" r="9"/><path d="M12 3a9 9 0 0 0 0 18z" fill="currentColor" stroke="none"/></svg>`;
41}
42
43const MODE_STORAGE_KEY = "zbin-mode";
44
45// Enables the (HTML-disabled) client-side radio now that JS is running, applies
46// the saved client/server preference (defaulting to client since JS is here),
47// and persists changes. Shared by the index (encrypt_mode) and show (decrypt_mode) pages.
48export function setupModeRadios(name: string) {
49 const radios = document.querySelectorAll<HTMLInputElement>(
50 `input[name="${name}"]`,
51 );
52 if (radios.length === 0) return;
53 const saved = localStorage.getItem(MODE_STORAGE_KEY) ?? "client";
54 for (const radio of radios) {
55 radio.disabled = false;
56 radio.checked = radio.value === saved;
57 radio.addEventListener("change", () => {
58 if (radio.checked) localStorage.setItem(MODE_STORAGE_KEY, radio.value);
59 });
60 }
61}
62
63export function getMode(name: string): string {
64 const checked = document.querySelector<HTMLInputElement>(
65 `input[name="${name}"]:checked`,
66 );
67 return checked?.value ?? "server";
68}
69