Files
miniSCP/ui/src/app/core/toast.service.ts
T
jgamboaandClaude 17bb1a14fb Stage 1: extract the skin-agnostic UI layer into app/core/
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>
2026-08-16 04:49:27 +02:00

64 lines
1.7 KiB
TypeScript

import { Injectable, signal } from '@angular/core';
export interface Toast {
id: number;
kind: 'error' | 'info' | 'success';
text: string;
ttlMs: number;
}
@Injectable({ providedIn: 'root' })
export class ToastService {
readonly toasts = signal<Toast[]>([]);
private nextId = 1;
private readonly timers = new Map<
number,
{ timer: ReturnType<typeof setTimeout>; expiresAt: number; remaining: number }
>();
error(text: string) {
this.show('error', text, 6000);
}
info(text: string) {
this.show('info', text, 3000);
}
success(text: string) {
this.show('success', text, 3000);
}
dismiss(id: number) {
const entry = this.timers.get(id);
if (entry) clearTimeout(entry.timer);
this.timers.delete(id);
this.toasts.update((list) => list.filter((t) => t.id !== id));
}
/** Freeze the auto-dismiss countdown (hover). */
pause(id: number) {
const entry = this.timers.get(id);
if (!entry) return;
clearTimeout(entry.timer);
entry.remaining = Math.max(0, entry.expiresAt - Date.now());
}
/** Resume the countdown with whatever time was left when paused. */
resume(id: number) {
const entry = this.timers.get(id);
if (!entry) return;
entry.expiresAt = Date.now() + entry.remaining;
entry.timer = setTimeout(() => this.dismiss(id), entry.remaining);
}
private show(kind: Toast['kind'], text: string, ttlMs: number) {
const toast = { id: this.nextId++, kind, text, ttlMs };
this.toasts.update((list) => [...list, toast]);
this.timers.set(toast.id, {
timer: setTimeout(() => this.dismiss(toast.id), ttlMs),
expiresAt: Date.now() + ttlMs,
remaining: ttlMs,
});
}
}