BlueSky & more on desktop lazurite.stormlightlabs.org/
tauri rust typescript bluesky appview atproto solid
at main 171 lines 4.1 kB view raw
1import type { LogEntry, Maybe } from "$/lib/types"; 2 3const MAX_JSON_PREVIEW_CHARS = 6000; 4 5export function escapeForRegex(value: string) { 6 return value.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`); 7} 8 9export function formatCount(value: Maybe<number>) { 10 if (!value) { 11 return "0"; 12 } 13 14 if (value >= 1000) { 15 return `${(value / 1000).toFixed(value >= 10_000 ? 0 : 1)}K`; 16 } 17 18 return value.toString(); 19} 20 21export function normalizeError(err: unknown): string { 22 if (err instanceof Error) { 23 return err.message; 24 } else { 25 return String(err); 26 } 27} 28 29export function formatEtaSeconds(value: number) { 30 if (value < 60) { 31 return `${value}s`; 32 } 33 34 const minutes = Math.floor(value / 60); 35 const seconds = value % 60; 36 return `${minutes}m ${seconds}s`; 37} 38 39export function formatProgress(value: number | null | undefined) { 40 if (typeof value !== "number" || Number.isNaN(value)) { 41 return "Pending"; 42 } 43 44 return `${Math.round(value)}%`; 45} 46 47export function formatBytes(bytes: number): string { 48 if (bytes === 0) return "0 B"; 49 const k = 1024; 50 const sizes = ["B", "KB", "MB", "GB"]; 51 const i = Math.floor(Math.log(bytes) / Math.log(k)); 52 return `${Number.parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`; 53} 54 55export function formatLogTimestamp(timestamp: string | null) { 56 if (!timestamp) { 57 return "--"; 58 } 59 60 const parsed = new Date(timestamp); 61 if (Number.isNaN(parsed.getTime())) { 62 return timestamp; 63 } 64 65 return parsed.toLocaleString(undefined, { 66 day: "2-digit", 67 hour: "2-digit", 68 minute: "2-digit", 69 month: "short", 70 second: "2-digit", 71 }); 72} 73 74export function formatLogCopyLine(log: LogEntry) { 75 const prefix = [formatLogTimestamp(log.timestamp), log.level, log.target ?? "app"].join(" "); 76 return `${prefix} ${log.message}`; 77} 78 79export function formatJoinedDate(value?: string | null) { 80 if (!value) { 81 return null; 82 } 83 84 const parsed = new Date(value); 85 if (Number.isNaN(parsed.getTime())) { 86 return null; 87 } 88 89 return parsed.toLocaleDateString(undefined, { month: "long", year: "numeric" }); 90} 91 92export function initials(name: string) { 93 return name.trim().slice(0, 1).toUpperCase() || "?"; 94} 95 96export function formatHandle(handle: string | null | undefined, did: string | null | undefined) { 97 if (!handle) { 98 return did ?? "Unknown"; 99 } 100 101 return handle.startsWith("did:") || handle.startsWith("@") ? handle : `@${handle}`; 102} 103 104export function clamp(value: number, min: number, max: number) { 105 return Math.min(Math.max(value, min), max); 106} 107 108export function hashString(value: string) { 109 let hash = 0x81_1C_9D_C5; 110 for (let index = 0; index < value.length; index += 1) { 111 hash ^= value.codePointAt(index)!; 112 hash = Math.imul(hash, 0x01_00_01_93); 113 } 114 115 return (hash >>> 0).toString(16).padStart(8, "0"); 116} 117 118export function stringifyUnknown(value: unknown) { 119 const seen = new WeakSet<object>(); 120 121 try { 122 const json = JSON.stringify(value, (_, current) => { 123 if (typeof current !== "object" || current === null) { 124 return current; 125 } 126 127 if (seen.has(current)) { 128 return "[Circular]"; 129 } 130 seen.add(current); 131 return current; 132 }, 2); 133 134 if (!json) { 135 return "null"; 136 } 137 138 if (json.length <= MAX_JSON_PREVIEW_CHARS) { 139 return json; 140 } 141 142 return `${json.slice(0, MAX_JSON_PREVIEW_CHARS)}\n...`; 143 } catch { 144 return String(value); 145 } 146} 147 148export function formatRelativeTime(value: string) { 149 const timestamp = new Date(value).getTime(); 150 if (Number.isNaN(timestamp)) { 151 return ""; 152 } 153 154 const deltaSeconds = Math.round((timestamp - Date.now()) / 1000); 155 const formatter = new Intl.RelativeTimeFormat(undefined, { numeric: "auto" }); 156 const ranges = [ 157 ["year", 60 * 60 * 24 * 365], 158 ["month", 60 * 60 * 24 * 30], 159 ["day", 60 * 60 * 24], 160 ["hour", 60 * 60], 161 ["minute", 60], 162 ] as const; 163 164 for (const [unit, seconds] of ranges) { 165 if (Math.abs(deltaSeconds) >= seconds) { 166 return formatter.format(Math.round(deltaSeconds / seconds), unit); 167 } 168 } 169 170 return formatter.format(deltaSeconds, "second"); 171}