index.ts (8827B)
1 import { browser } from '$app/environment'; 2 import { goto } from '$app/navigation'; 3 import { win_h, win_w } from '$lib/stores/app'; 4 import type { CallbackRoute, NavigationParamTuple, NavigationRouteParamKey, NavigationRouteParamTuple, } from '$lib/types/lib'; 5 import type { ThemeLayer, ThemeMode } from '@radroots/themes'; 6 import type { WebFilePath } from '@radroots/utils'; 7 import { getContext, setContext } from "svelte"; 8 import { get } from "svelte/store"; 9 10 export const SYMBOLS = { 11 bullet: '•', 12 dash: `—`, 13 up: `↑`, 14 down: `↓`, 15 percent: `%` 16 }; 17 18 export const get_store = get; 19 20 export const get_context = <T>(key: string): T => 21 getContext<T>(key); 22 23 export const set_context = <T>(key: string, value: T): T => 24 setContext(key, value); 25 26 export const sleep = (ms: number): Promise<void> => 27 new Promise((r) => setTimeout(r, ms)); 28 29 export const trim_slashes = (path: string): string => 30 path.replace(/^\/+|\/+$/g, ''); 31 32 export const normalize_path = (path: string): string => 33 path 34 .replace(/-/g, '_') 35 .replace(/\//g, '-') 36 .replace(/-+/g, '-'); 37 38 export const sanitize_path = (id: string): string => 39 id.replace(/[^A-Za-z0-9_-]+/g, ''); 40 41 export const fmt_id = (raw_id?: string): string => { 42 if (!browser) return ''; 43 const pathname = window.location.pathname; 44 const trimmed = trim_slashes(pathname); 45 const prefix = normalize_path(trimmed); 46 const suffix = raw_id ? `-${sanitize_path(raw_id)}` : ''; 47 return `*${prefix}${suffix}`; 48 }; 49 50 export const view_effect = <T extends string>(view: T): void => { 51 if (!browser) return; 52 for (const el of document.querySelectorAll(`[data-view]`)) { 53 if (el.getAttribute(`data-view`) !== view) el.classList.add(`hidden`) 54 else el.classList.remove(`hidden`) 55 } 56 }; 57 58 export const el_id = (id: string): HTMLElement | undefined => { 59 if (!browser) return undefined; 60 const el = document.getElementById(id); 61 return el ? el : undefined; 62 }; 63 64 export const build_storage_key = ( 65 raw_id: string, 66 base_prefix: string 67 ): string => 68 `${fmt_id()}-${sanitize_path(raw_id)}` 69 .replace(new RegExp(`^\\*${normalize_path(trim_slashes(base_prefix))}-?`), '*'); 70 71 export const get_system_theme = (fallback: ThemeMode = "light"): ThemeMode => { 72 if (!browser) return fallback; 73 return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; 74 }; 75 76 export const theme_set = (theme_key: string, color_mode: ThemeMode): void => { 77 if (!browser) return; 78 document.documentElement.setAttribute("data-theme", `${theme_key}_${color_mode}`); 79 }; 80 export const fmt_cl = (classes?: string): string => `${classes || ``}`; 81 82 export const handle_err = async (e: unknown, fcall: string): Promise<void> => { 83 try { 84 console.log(`[handle_err] `, e, fcall) 85 /*return void await catch_err(e, fcall, async (opts) => { 86 console.log(`handle_err e `, e) 87 console.log(JSON.stringify(opts, null, 4), `handle_err opts`) 88 });*/ 89 } catch (e) { 90 console.log(`(handle_err) `, e) 91 } 92 }; 93 94 export const window_set = (): void => { 95 if (!browser) return; 96 win_h.set(window.innerHeight); 97 win_w.set(window.innerWidth); 98 }; 99 100 export const parse_layer = (layer?: number, layer_default?: ThemeLayer): ThemeLayer => { 101 switch (layer) { 102 case 0: 103 case 1: 104 case 2: 105 return layer; 106 default: 107 return layer_default ?? 0; 108 }; 109 }; 110 111 export const value_constrain = (regex_charset: RegExp, value: string): string => { 112 return value 113 .split(``) 114 .filter((char) => regex_charset.test(char)) 115 .join(``); 116 }; 117 118 119 export const encode_query_params = <T extends string>(params_list: NavigationParamTuple<T>[] = []): string => { 120 let query = ""; 121 for (const [k, v] of params_list) { 122 if (k && v) { 123 if (query) query += `&`; 124 query += `${k.trim()}=${encodeURIComponent(v.trim())}`; 125 } 126 } 127 return query ? `?${query}` : ``; 128 }; 129 130 export const encode_route = <TRoute extends string, TParam extends string>(route: TRoute, params_list?: NavigationParamTuple<TParam>[]): string => { 131 const query = encode_query_params(params_list); 132 if (!query) return route; 133 return `${route === `/` ? `/` : route.replace(/\/+$/, ``)}${query}`; 134 }; 135 136 export const debounce = <TArgs extends readonly unknown[]>( 137 fn: (...args: TArgs) => void, 138 delay: number 139 ): ((...args: TArgs) => void) => { 140 let timeout: ReturnType<typeof setTimeout> | undefined; 141 return (...args: TArgs) => { 142 if (timeout) clearTimeout(timeout); 143 timeout = setTimeout(() => fn(...args), delay); 144 }; 145 }; 146 147 export const create_router = <T extends string>(): ((nav_route: T, params?: NavigationRouteParamTuple[]) => Promise<void>) => { 148 const router = async (nav_route: T, params: NavigationRouteParamTuple[] = []): Promise<void> => { 149 try { 150 if (params.length) await goto(encode_route<T, NavigationRouteParamKey>(nav_route, params)); 151 else await goto(nav_route); 152 } catch (e) { 153 handle_err(e, `route`); 154 }; 155 }; 156 return router; 157 }; 158 159 export const get_locale = (locales: string[]): string => { 160 if (!browser) return (locales[0] ?? `en`).toLowerCase(); 161 const { language: navigator_locale } = navigator; 162 let locale = `en`; 163 if (locales.some(i => i === navigator_locale.toLowerCase())) locale = navigator.language; 164 else if (locales.some(i => i === navigator_locale.slice(0, 2).toLowerCase())) locale = navigator_locale.slice(0, 2); 165 return locale.toLowerCase(); 166 }; 167 168 export const callback_route = async <T extends string>(callback_route: CallbackRoute<T>): Promise<void> => { 169 if (`route` in callback_route) { 170 if (typeof callback_route.route === `string`) return void await goto(callback_route.route); 171 else return void await goto( 172 encode_route<string, NavigationRouteParamKey>( 173 callback_route.route[0], 174 callback_route.route[1], 175 ), 176 ); 177 } 178 return void await callback_route(); 179 }; 180 181 export const to_arr_buf = (u8: Uint8Array): ArrayBuffer => { 182 if (u8.byteOffset === 0 && u8.byteLength === u8.buffer.byteLength && u8.buffer instanceof ArrayBuffer) return u8.buffer; 183 if (u8.buffer instanceof ArrayBuffer) return u8.buffer.slice(u8.byteOffset, u8.byteOffset + u8.byteLength); 184 const copy = new Uint8Array(u8.byteLength); 185 copy.set(u8); 186 return copy.buffer; 187 }; 188 189 export const parse_file_path = (file_path: string): WebFilePath | undefined => { 190 if (file_path.startsWith("blob:")) return { blob_path: file_path, blob_name: file_path.replaceAll("blob:", "").replaceAll("http://", "") }; 191 const file_path_spl = file_path.split(`/`); 192 const file_path_file = file_path_spl[file_path_spl.length - 1] || ``; 193 const [file_name, mime_type] = file_path_file.split(`.`); 194 if (!file_name || !mime_type) return undefined; 195 return { 196 file_path, 197 file_name, 198 mime_type 199 }; 200 }; 201 202 export const download_json = <T>(data: T, filename: string): void => { 203 if (!browser) return; 204 const json = JSON.stringify(data, null, 2); 205 const blob = new Blob([json], { type: "application/json" }); 206 const url = URL.createObjectURL(blob); 207 const anchor = document.createElement("a"); 208 anchor.href = url; 209 anchor.download = filename; 210 anchor.click(); 211 URL.revokeObjectURL(url); 212 }; 213 214 export const select_file = async (): Promise<File | undefined> => { 215 if (!browser) return undefined; 216 return new Promise((resolve) => { 217 const input = document.createElement("input"); 218 input.type = "file"; 219 input.accept = "*/*"; 220 input.style.display = "none"; 221 const cleanup = () => { 222 input.remove(); 223 }; 224 input.addEventListener("change", async () => { 225 const file = input.files?.[0]; 226 cleanup(); 227 resolve(file ?? undefined); 228 }); 229 document.body.appendChild(input); 230 input.click(); 231 }); 232 }; 233 234 export const get_file_text = async (file: File | null): Promise<string | undefined> => { 235 if (!file) return undefined; 236 const text = await file.text(); 237 return text; 238 }; 239 240 export type ParseJsonResult = { ok: true; value: unknown } | { ok: false; error: Error }; 241 242 export const parse_file_json = async (file: File | null): Promise<ParseJsonResult> => { 243 const contents = await get_file_text(file); 244 if (!contents) return { ok: false, error: new Error("empty_file") }; 245 try { 246 const parsed: unknown = JSON.parse(contents); 247 return { ok: true, value: parsed }; 248 } catch (error) { 249 const err = error instanceof Error ? error : new Error("invalid_json"); 250 return { ok: false, error: err }; 251 } 252 };