First step of splitting the UI into two skin trees. All 12 services plus platform.ts, format.ts, file-icons.ts, icons.ts and app.config.ts move to ui/src/app/core/ — the layer both skins will share. This is a pure move: git records all 17 as renames, and no file content changed beyond import paths. The premise holds because `git diff main...feat/7css-skin` touches none of these files — the service layer was already skin-agnostic, so there is nothing to untangle, only to relocate. Components now reach it through a new `@core/*` tsconfig alias rather than a relative path. That is deliberate and load-bearing for the next stage: skin components are about to move under app/skins/<skin>/ at varying depths, and an alias keeps every one of these 67 imports correct wherever its file ends up. `@shared/*` (the bun-side src/shared) already set the precedent. Intra-core imports needed no edits at all — every ./sibling reference among the moved files points at another moved file. Verified: tsc clean, `bun run build:ui` clean (AOT resolves every import and template symbol), and the running window's opaque interior is pixel-identical to a pre-refactor capture. Note on that comparison: the Aero frame and title bar do NOT compare equal between launches, and that is not a regression — .window.glass::before runs at opacity .6, so the chrome composites over whatever desktop is behind the window and shifts by a uniform ~8% brightness. The check therefore diffs the opaque interior (inset 30px sides, 80px top), where anything this refactor could break would show. Co-Authored-By: Claude <noreply@anthropic.com>
85 lines
2.8 KiB
TypeScript
85 lines
2.8 KiB
TypeScript
const SIZE_UNITS = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
|
|
/** Locale-aware number formatting (decimal separator); cached per locale. */
|
|
const NUM_FORMATS = new Map<string, Intl.NumberFormat>();
|
|
function num(value: number, locale: string | undefined): string {
|
|
const key = locale ?? '';
|
|
let fmt = NUM_FORMATS.get(key);
|
|
if (!fmt) {
|
|
fmt = new Intl.NumberFormat(locale, { maximumFractionDigits: 1 });
|
|
NUM_FORMATS.set(key, fmt);
|
|
}
|
|
return fmt.format(value);
|
|
}
|
|
|
|
export function formatSize(bytes: number, locale?: string): string {
|
|
let value = bytes;
|
|
let unit = 0;
|
|
while (value >= 1024 && unit < SIZE_UNITS.length - 1) {
|
|
value /= 1024;
|
|
unit++;
|
|
}
|
|
return `${unit === 0 ? num(value, locale) : num(Number(value.toFixed(1)), locale)} ${SIZE_UNITS[unit]}`;
|
|
}
|
|
|
|
const DATE_FORMATS = new Map<string, Intl.DateTimeFormat>();
|
|
export function formatDate(epochMs: number, locale?: string): string {
|
|
if (!epochMs) return '';
|
|
const key = locale ?? '';
|
|
let fmt = DATE_FORMATS.get(key);
|
|
if (!fmt) {
|
|
fmt = new Intl.DateTimeFormat(locale, {
|
|
year: '2-digit',
|
|
month: '2-digit',
|
|
day: '2-digit',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
});
|
|
DATE_FORMATS.set(key, fmt);
|
|
}
|
|
return fmt.format(new Date(epochMs));
|
|
}
|
|
|
|
/** bytes/sec -> "14.2 MB/s" or "113.8 Mbps" depending on the configured unit. */
|
|
export function formatSpeed(bytesPerSec: number, unit: 'MBps' | 'Mbps', locale?: string): string {
|
|
if (unit === 'Mbps') {
|
|
return `${num(Number(((bytesPerSec * 8) / 1_000_000).toFixed(1)), locale)} Mbps`;
|
|
}
|
|
return `${formatSize(bytesPerSec, locale)}/s`;
|
|
}
|
|
|
|
/** Seconds -> "0:42" / "3:07" / "1:02:15". */
|
|
export function formatEta(seconds: number): string {
|
|
if (!Number.isFinite(seconds) || seconds < 0) return '';
|
|
const s = Math.round(seconds);
|
|
const h = Math.floor(s / 3600);
|
|
const m = Math.floor((s % 3600) / 60);
|
|
const sec = s % 60;
|
|
const mmss = `${m}:${String(sec).padStart(2, '0')}`;
|
|
return h ? `${h}:${String(m).padStart(2, '0')}:${String(sec).padStart(2, '0')}` : mmss;
|
|
}
|
|
|
|
/**
|
|
* Separator-aware join for UI-composed paths: remote and POSIX-local dirs use
|
|
* "/", a Windows-local dir ("C:\Users\x") keeps "\" so paths stay consistent.
|
|
*/
|
|
export function joinPath(dir: string, name: string): string {
|
|
const sep = dir.includes('\\') && !dir.includes('/') ? '\\' : '/';
|
|
return dir.endsWith(sep) ? dir + name : `${dir}${sep}${name}`;
|
|
}
|
|
|
|
/** "/" (POSIX) or a drive root like "C:\" — nothing above to navigate to. */
|
|
export function isRootPath(p: string): boolean {
|
|
return p === '/' || /^[A-Za-z]:[\\/]?$/.test(p);
|
|
}
|
|
|
|
/** 0o755 -> "rwxr-xr-x" */
|
|
export function formatMode(mode: number): string {
|
|
const flags = 'rwxrwxrwx';
|
|
let out = '';
|
|
for (let i = 0; i < 9; i++) {
|
|
out += mode & (0o400 >> i) ? flags[i] : '-';
|
|
}
|
|
return out;
|
|
}
|