refactor(desktop): organize lib modules by domain

This commit is contained in:
t8y2 committed 2026-07-04 13:53:52 +08:00
1 parent 90f0594e93
commit 7e7c4944e9
700 files changed
+1292 -1264

No files matched your search

+155
View File
@@ -0,0 +1,155 @@
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
interface ClipboardApi {
readText?: () => Promise<string> | string;
writeText?: (text: string) => Promise<void> | void;
}
interface ClipboardNavigator {
clipboard?: ClipboardApi;
}
interface ClipboardTextarea {
value: string;
style: {
position?: string;
top?: string;
left?: string;
opacity?: string;
};
setAttribute(name: string, value: string): void;
focus?(): void;
select(): void;
setSelectionRange?(start: number, end: number): void;
}
interface ClipboardDocument {
body?: {
appendChild(node: unknown): unknown;
removeChild(node: unknown): unknown;
};
createElement(tagName: "textarea"): ClipboardTextarea;
execCommand?(command: string): boolean;
}
export interface ClipboardEnvironment {
navigator?: ClipboardNavigator;
document?: ClipboardDocument;
}
export interface ClipboardShortcutEvent {
key: string;
metaKey?: boolean;
ctrlKey?: boolean;
altKey?: boolean;
shiftKey?: boolean;
target?: EventTarget | null;
}
interface SelectionLike {
anchorNode: Node | null;
focusNode: Node | null;
isCollapsed: boolean;
}
interface NativeClipboardSelectionEnvironment {
getSelection?: () => SelectionLike | null;
}
const EDITABLE_CLIPBOARD_TARGET_SELECTOR = "input, textarea, [contenteditable='true'], [role='textbox']";
const NATIVE_CLIPBOARD_REGION_SELECTOR = "[data-native-clipboard]";
function closestElement(target: unknown, selector: string): unknown {
return (target as { closest?: (selector: string) => unknown } | null)?.closest?.(selector) ?? null;
}
function selectionNodeElement(node: Node | null): Element | null {
if (!node) return null;
if (typeof Element !== "undefined" && node instanceof Element) return node;
return (node as { parentElement?: Element | null }).parentElement ?? null;
}
export function isPlainClipboardShortcut(event: ClipboardShortcutEvent, key: string): boolean {
return !!(event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey && event.key.toLowerCase() === key;
}
export function hasNativeClipboardSelection(env: NativeClipboardSelectionEnvironment = globalThis as unknown as NativeClipboardSelectionEnvironment): boolean {
const selection = env.getSelection?.();
if (!selection || selection.isCollapsed) return false;
const anchorRegion = selectionNodeElement(selection.anchorNode)?.closest(NATIVE_CLIPBOARD_REGION_SELECTOR);
const focusRegion = selectionNodeElement(selection.focusNode)?.closest(NATIVE_CLIPBOARD_REGION_SELECTOR);
return !!anchorRegion && anchorRegion === focusRegion;
}
export function eventTargetAllowsNativeClipboard(event: ClipboardShortcutEvent, env: NativeClipboardSelectionEnvironment = globalThis as unknown as NativeClipboardSelectionEnvironment): boolean {
if (closestElement(event.target, EDITABLE_CLIPBOARD_TARGET_SELECTOR)) return true;
return isPlainClipboardShortcut(event, "c") && hasNativeClipboardSelection(env);
}
export function eventTargetAllowsAppClipboardShortcut(event: ClipboardShortcutEvent, key = "v"): boolean {
if (!isPlainClipboardShortcut(event, key)) return false;
return !closestElement(event.target, EDITABLE_CLIPBOARD_TARGET_SELECTOR);
}
export async function readTextFromClipboard(env: ClipboardEnvironment = globalThis as unknown as ClipboardEnvironment): Promise<string> {
if (isTauriRuntime(env as unknown as Record<string, unknown>)) {
try {
const { readText } = await import("@tauri-apps/plugin-clipboard-manager");
return await readText();
} catch {
// Fall through to Web Clipboard when the native plugin is unavailable.
}
}
if (env.navigator?.clipboard?.readText) {
return await env.navigator.clipboard.readText();
}
throw new Error("Clipboard API is not available");
}
export async function copyToClipboard(text: string, env: ClipboardEnvironment = globalThis as unknown as ClipboardEnvironment): Promise<void> {
if (isTauriRuntime(env as unknown as Record<string, unknown>)) {
try {
const { writeText } = await import("@tauri-apps/plugin-clipboard-manager");
await writeText(text);
return;
} catch {
// Fall through to Web Clipboard / legacy copy when the native plugin is unavailable.
}
}
try {
if (env.navigator?.clipboard?.writeText) {
await env.navigator.clipboard.writeText(text);
return;
}
} catch {
// Fall through to the legacy copy path for non-secure web contexts.
}
const document = env.document;
if (!document?.body || !document.execCommand) {
throw new Error("Clipboard API is not available");
}
const textarea = document.createElement("textarea");
textarea.value = text;
textarea.setAttribute("readonly", "");
textarea.style.position = "fixed";
textarea.style.top = "0";
textarea.style.left = "-9999px";
textarea.style.opacity = "0";
document.body.appendChild(textarea);
try {
textarea.focus?.();
textarea.select();
textarea.setSelectionRange?.(0, text.length);
if (!document.execCommand("copy")) {
throw new Error("Clipboard copy failed");
}
} finally {
document.body.removeChild(textarea);
}
}
+30
View File
@@ -0,0 +1,30 @@
export function hexToRgba(color: string | undefined, alpha: number): string | undefined {
if (!color) return undefined;
const trimmed = color.trim();
// Already rgba/rgb: replace or add alpha, then return
const rgbaMatch = trimmed.match(/^rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*([\d.]+)\s*\)$/i);
if (rgbaMatch) {
return `rgba(${rgbaMatch[1]}, ${rgbaMatch[2]}, ${rgbaMatch[3]}, ${alpha})`;
}
const rgbMatch = trimmed.match(/^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/i);
if (rgbMatch) {
return `rgba(${rgbMatch[1]}, ${rgbMatch[2]}, ${rgbMatch[3]}, ${alpha})`;
}
const hex = trimmed.replace(/^#/, "");
const normalized =
hex.length === 3
? hex
.split("")
.map((c) => c + c)
.join("")
: hex;
if (!/^[0-9a-fA-F]{6}$/.test(normalized)) return undefined;
const value = Number.parseInt(normalized, 16);
const r = (value >> 16) & 255;
const g = (value >> 8) & 255;
const b = value & 255;
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
}
@@ -0,0 +1,30 @@
export function expandNestedJsonStringsForCopy(value: unknown): unknown {
if (typeof value === "string") {
return parseNestedJsonString(value) ?? value;
}
if (Array.isArray(value)) {
return value.map(expandNestedJsonStringsForCopy);
}
if (isPlainObject(value)) {
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, expandNestedJsonStringsForCopy(item)]));
}
return value;
}
function parseNestedJsonString(value: string): unknown | undefined {
const trimmed = value.trim();
if (!trimmed || (trimmed[0] !== "{" && trimmed[0] !== "[")) return undefined;
try {
return expandNestedJsonStringsForCopy(JSON.parse(trimmed));
} catch {
return undefined;
}
}
function isPlainObject(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
@@ -0,0 +1,15 @@
export function parseSlashDelimitedRegexQuery(query: string): RegExp | null {
if (!query.startsWith("/") || query.length < 2) return null;
const lastSlash = query.lastIndexOf("/");
if (lastSlash <= 0) return null;
const source = query.slice(1, lastSlash);
const flags = query.slice(lastSlash + 1);
if (!/^[dgimsuvy]*$/.test(flags)) return null;
try {
return new RegExp(source, flags.includes("i") ? flags : `${flags}i`);
} catch {
return null;
}
}
+26
View File
@@ -0,0 +1,26 @@
/**
* Formats Redis TTL (seconds) as a compact string:
* < 1 day → HH:mm:ss
* >= 1 day → {n}d HH:mm:ss (day unit localized)
*
* Returns null for ttl === -1 (no expiry) or ttl <= 0.
*/
type TranslateFn = (key: string, options?: Record<string, unknown>) => string;
export function formatTtl(ttl: number, t: TranslateFn): string | null {
if (ttl === -1) return null;
if (ttl <= 0) return null;
const days = Math.floor(ttl / 86_400);
const hours = Math.floor((ttl % 86_400) / 3_600);
const minutes = Math.floor((ttl % 3_600) / 60);
const seconds = ttl % 60;
const time = [String(hours).padStart(2, "0"), String(minutes).padStart(2, "0"), String(seconds).padStart(2, "0")].join(":");
if (days > 0) {
return `${t("redis.ttlDay", { count: days })} ${time}`;
}
return time;
}
+17
View File
@@ -0,0 +1,17 @@
import type { ClassValue } from "clsx";
import { clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export function uuid(): string {
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
return crypto.randomUUID();
}
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
return (c === "x" ? r : (r & 0x3) | 0x8).toString(16);
});
}
+39
View File
@@ -0,0 +1,39 @@
function normalizeBasePath(value: string | undefined): string {
const trimmed = (value ?? "").trim();
if (!trimmed || trimmed === "." || trimmed === "./" || trimmed === "/") return "";
const withoutQuery = trimmed.split(/[?#]/, 1)[0] ?? "";
const withLeadingSlash = withoutQuery.startsWith("/") ? withoutQuery : `/${withoutQuery}`;
return withLeadingSlash.replace(/\/+$/, "");
}
function inferredRuntimeBasePath(pathname: string): string {
const normalized = pathname.replace(/\/+$/, "");
if (!normalized || normalized === "/login") return "";
if (normalized.endsWith("/login")) return normalized.slice(0, -"/login".length);
return normalized;
}
export function dbxWebBasePath(pathname = globalThis.location?.pathname ?? "", buildBase = import.meta.env.BASE_URL): string {
const configured = normalizeBasePath(buildBase);
if (configured) return configured;
return normalizeBasePath(inferredRuntimeBasePath(pathname));
}
export function webPath(path: string, basePath = dbxWebBasePath()): string {
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
const base = normalizeBasePath(basePath);
return `${base}${normalizedPath}` || "/";
}
export function apiUrl(path: string, basePath = dbxWebBasePath()): string {
const pathWithLeadingSlash = path.startsWith("/") ? path : `/${path}`;
const normalizedPath = pathWithLeadingSlash === "/api" || pathWithLeadingSlash.startsWith("/api/") || pathWithLeadingSlash.startsWith("/api?") ? pathWithLeadingSlash : `/api${pathWithLeadingSlash}`;
return webPath(normalizedPath, basePath);
}
type WebSocketLocation = Pick<Location, "protocol" | "host"> | undefined;
export function apiWebSocketUrl(path: string, basePath = dbxWebBasePath(), currentLocation: WebSocketLocation = globalThis.location): string {
const protocol = currentLocation?.protocol === "https:" ? "wss:" : "ws:";
return `${protocol}//${currentLocation?.host ?? ""}${apiUrl(path, basePath)}`;
}