// biome-ignore lint/correctness/noUnusedImports: Html is the JSX factory (tsconfig jsxFactory: Html.createElement) import { Html } from "@elysiajs/html"; import type { PropsWithChildren } from "@kitajs/html"; import { escapeHTML } from "bun"; import hljs from "highlight.js"; import { config } from "./config"; import { humanFileSize, isValidUTF8, type Theme, themeIcon } from "./shared"; export const filetypes = ["none", "blob"].concat(hljs.listLanguages().sort()); // The auto/light/dark toggle. A plain form so it works without JS (it POSTs to // /theme/cycle, which flips the cookie and redirects back); client-theme.js // upgrades it to switch instantly without a reload. function ThemeToggle(props: { theme: Theme }) { return (
); } function Template(props: PropsWithChildren<{ css: string; theme: Theme }>) { // Server-render the active theme so it's correct before first paint (no flash) // and works with JS off. The dark sakura sheet is gated by `media`: in auto it // follows the OS, otherwise it's forced fully on/off. const darkMedia = props.theme === "auto" ? "(prefers-color-scheme: dark)" : props.theme === "dark" ? "all" : "not all"; return ( <> {""}File not found
); } // Surfaces the server's configured abuse limits so users know the rules before // uploading. Each line only shows when the corresponding limit is actually set. function ServerLimits() { return (Reload page to try again
); } export function humanReadableTime(minutes: number) { // Guard on the total before it is broken into units below; an already-expired // (or sub-minute) duration has no meaningful breakdown. if (minutes <= 0) return "expired"; const years = Math.floor(minutes / 525600); minutes %= 525600; const days = Math.floor(minutes / 1440); minutes %= 1440; const hours = Math.floor(minutes / 60); minutes %= 60; let result = ""; if (years > 0) result += `${years} year${years > 1 ? "s" : ""} `; if (days > 0) result += `${days} day${days > 1 ? "s" : ""} `; if (hours > 0) result += `${hours} hour${hours > 1 ? "s" : ""} `; if (minutes > 0) result += `${minutes} minute${minutes > 1 ? "s" : ""} `; if (result === "") result = "0 minutes"; return result.trim(); } // Renders content as escaped/highlighted HTML for a text preview, or returns // null if it isn't previewable text (binary, or the "blob" override). The // returned string is already HTML-safe and is injected raw into a.
export function textPreviewHtml(
content: Uint8Array,
filetype: string,
): string | null {
if (filetype === "blob" || !isValidUTF8(content)) return null;
const text = new TextDecoder("utf-8").decode(content);
if (filetype === "none") return escapeHTML(text);
return hljs.highlight(text, { language: filetype }).value;
}
// What the /show page should render in the preview area. Computed server-side
// in index.ts so that ShowFile never has to decrypt or read content itself.
export type Preview =
| { kind: "await" } // encrypted, needs client-side decryption (overlay + JS)
| { kind: "text"; html: string } // pre-rendered (escaped/highlighted) text
| { kind: "media"; mime: string } //
/