export function humanFileSize(size: number) {
if (size < 1024) return `${size} B`;
const i = Math.min(4, Math.floor(Math.log(size) / Math.log(1024)));
return `${(size / 1024 ** i).toFixed(2)} ${["B", "KiB", "MiB", "GiB", "TiB"][i]}`;
}
// TextDecoder-based UTF-8 check that works in both Bun and the browser.
export function isValidUTF8(buf: ArrayBuffer | ArrayBufferView): boolean {
try {
new TextDecoder("utf-8", { fatal: true }).decode(buf);
return true;
} catch (_e) {
return false;
}
}
export type Theme = "auto" | "light" | "dark";
// Reads the persisted theme choice from a raw Cookie request header, defaulting
// to "auto" (follow the OS) when unset or unrecognized. Values are plain
// (auto/light/dark), so no decoding is needed.
export function getTheme(cookieHeader: string | undefined): Theme {
const value = cookieHeader?.match(/(?:^|;\s*)zbin-theme=([^;]+)/)?.[1];
return value === "light" || value === "dark" ? value : "auto";
}
// Inline SVG for the toggle's current state, drawn in `currentColor` so it picks
// up the button's text color. Shared by the server (initial render) and
// client-theme.js (on toggle): light = sun, dark = crescent, auto = a half-filled
// disc suggesting "follows the system". Injected as raw markup (not escaped).
export function themeIcon(theme: Theme): string {
const open =
'`;
}
if (theme === "dark") {
return `${open}`;
}
return `${open}`;
}
const MODE_STORAGE_KEY = "zbin-mode";
// Enables the (HTML-disabled) client-side radio now that JS is running, applies
// the saved client/server preference (defaulting to client since JS is here),
// and persists changes. Shared by the index (encrypt_mode) and show (decrypt_mode) pages.
export function setupModeRadios(name: string) {
const radios = document.querySelectorAll(
`input[name="${name}"]`,
);
if (radios.length === 0) return;
const saved = localStorage.getItem(MODE_STORAGE_KEY) ?? "client";
for (const radio of radios) {
radio.disabled = false;
radio.checked = radio.value === saved;
radio.addEventListener("change", () => {
if (radio.checked) localStorage.setItem(MODE_STORAGE_KEY, radio.value);
});
}
}
export function getMode(name: string): string {
const checked = document.querySelector(
`input[name="${name}"]:checked`,
);
return checked?.value ?? "server";
}