Files
miniSCP/src/shared/types.ts
T
jgamboaandClaude f101257f20 Scale stage 2: continuous uiScale setting + the (-) 100% (+) stepper
AppSettings.textSize (compact|default|large) becomes uiScale: number, 0.5-4
in 0.1 steps, default 1. One shared clampUiScale (next to the
effectiveConcurrency precedent) defines "valid" for the store, the service
and the stepper: clamp to range, snap to the step grid, strip float noise,
and map NaN/Infinity to 1.

- SettingsStore.load migrates legacy files (compact/default/large ->
  0.9/1/1.2, never over an explicit uiScale) and clamps ALWAYS, not just on
  migration — buildPreload writes this value into every window's root
  font-size, so a hand-edited settings.json (uiScale: 100) must not be able
  to render the UI unusable. Verified against 100, 0.001, -3, NaN, Infinity
  and float noise; migration verified against a real legacy file.
- SettingsService: applyUiScale sets the root font-size (16 * scale, removed
  entirely at 1 per the delete-when-default rule). Rides the existing
  settingsChanged broadcast, so open editor/manager windows follow live.
- buildPreload bakes the same font-size so the first paint is already scaled
  — without it every window opens at 100% and snaps.
- Both tab-bars: the 3-button Interface row becomes the stepper. Clicking the
  percentage resets to 100% (tooltip says so). i18n: compact/default/large
  dropped, smaller/larger/reset added, all 7 locales.

Verified live, win7 + modern: scale 1 is pixel-identical to the pre-work
baseline (opaque interior diff exactly 0); 0.5x is a real dense mode (~2x
rows); 2x scales title bar, tabs, rows, icons and controls together with text
contained in its boxes — which is what stage 1c's 7.css bridge bought; 4x is
enormous but coherent, and the win7 title-bar controls stay on-screen at the
default window size. The known caveat stands: no minimum window size exists,
so 4x in a small window is legal but cramped.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-16 18:09:59 +02:00

191 lines
6.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
export type EntryType = "file" | "dir" | "symlink";
export interface FileEntry {
name: string;
path: string; // absolute path on its filesystem
type: EntryType;
size: number; // bytes
mtime: number; // epoch milliseconds
mode: number; // permission bits only (masked & 0o7777)
owner?: string; // resolved name when the provider knows it
group?: string;
}
/** Optional file-list columns (Name is always shown). */
export type ColumnKey = "size" | "mtime" | "kind" | "rights" | "owner" | "group";
/** Every RPC response uses this envelope; errors never cross the bridge as exceptions. */
export type Result<T = object> = ({ ok: true } & T) | { ok: false; error: string };
export type AuthConfig =
| { kind: "password"; password: string }
| { kind: "key"; privateKeyPath: string; passphrase?: string }
| { kind: "agent" };
export interface ConnectionConfig {
host: string;
port: number;
username: string;
auth: AuthConfig;
initialPath?: string | null;
}
export interface SavedConnection extends ConnectionConfig {
id: string;
stackId: string | null;
name: string;
/** Other-panel local dir applied when that panel is local and untouched. */
localDir?: string | null;
}
/** A named group of connections that can be opened together as tabs. */
export interface Stack {
id: string;
name: string;
/** Collapsed in the connection-manager list (UI state, persisted). */
collapsed?: boolean;
}
/** A concrete host alias resolved from ~/.ssh/config (read-only). */
export interface SshConfigHost {
name: string; // the Host alias as written
hostname: string; // resolved HostName (defaults to alias)
port: number;
username: string;
identityFile?: string; // first existing IdentityFile, absolute path
unsupported?: "proxy"; // ProxyJump/ProxyCommand present — direct connect can't work
}
export interface ConnectionsFile {
version: 2;
stacks: Stack[];
connections: SavedConnection[];
}
/** UI languages the app ships translations for. */
export type SupportedLocale = "en" | "es" | "eu" | "fr" | "de" | "zh" | "ja";
export type AccentTone =
| "graphite"
| "blue"
| "indigo"
| "purple"
| "rose"
| "orange"
| "green"
| "teal";
export interface AppSettings {
/** UI language; "system" resolves to the OS locale (fallback English). */
locale: "system" | SupportedLocale;
/**
* Visual skin. "modern" is the shadcn-style look; "win7" swaps in 7.css
* (Windows 7 Aero) and switches the window to custom frameless chrome.
* Each skin is a whole component tree under ui/src/app/skins/ — "modern" is
* shadlike/, "win7" is 7like/ — selected at bootstrap in ui/src/main.ts.
* The value is NOT named after its folder on purpose: it is persisted in
* user settings, and "win7" is also the literal class 7.scoped.css is
* precompiled to require, so neither can be renamed freely.
* Changing this needs a window reopen to take effect on the frame itself —
* the CSS half applies live.
*/
skin: "modern" | "win7";
theme: "system" | "light" | "dark";
/** Dark appearance: grey ("dark") or near-black AMOLED ("darker"). */
darkTone: "dark" | "darker";
/** Accent tone for chrome and selection tint; graphite = neutral zinc. */
accent: AccentTone;
/** File-list icons: plain neutral glyphs, or per-category glyphs and colors. */
iconColors: "plain" | "multi";
/** Visible optional file-list columns (both panels; Name always shows). */
columns: ColumnKey[];
speedUnit: "MBps" | "Mbps";
/**
* Interface scale factor, 0.5–4 in 0.1 steps (shown as 50%–400%). Works by
* setting the root font-size — every size in the UI is rem, so one lever
* scales BOTH skins whole. Replaced the 3-tier `textSize`
* (compact|default|large); SettingsStore.load migrates old files.
*/
uiScale: number;
/** Run transfer jobs one at a time, or several in parallel. */
transferMode: "single" | "multi";
/** How many jobs run at once when transferMode is "multi". */
transferConcurrency: number;
/** Remove completed transfers from the list automatically. */
autoClearCompleted: boolean;
/** Edit files with the built-in editor or an external application. */
editor: "builtin" | "custom";
/** Full path to the external editor .app; empty = OS default application. */
editorApp: string;
// Auto-refresh the active tab's panels (local watched, remote polled). Always
// on for now; uncomment (+ the settings.service and tab-bar breadcrumbs, and
// gate the poll/watch in panels.service) to expose a toggle later.
// autoRefresh: boolean;
}
export const DEFAULT_SETTINGS: AppSettings = {
locale: "system",
skin: "modern",
theme: "system",
darkTone: "dark",
accent: "blue",
iconColors: "multi",
columns: ["size", "mtime"],
speedUnit: "MBps",
uiScale: 1,
transferMode: "single",
transferConcurrency: 3,
autoClearCompleted: true,
editor: "builtin",
editorApp: "",
// autoRefresh: true,
};
export const UI_SCALE_MIN = 0.5;
export const UI_SCALE_MAX = 4;
export const UI_SCALE_STEP = 0.1;
/**
* Clamp + snap a scale factor to the legal range and step grid. Shared by the
* store (a hand-edited settings.json must not be able to render the UI
* unusable), the settings service and the stepper — one definition of "valid",
* same as effectiveConcurrency below.
*/
export function clampUiScale(value: number): number {
if (!Number.isFinite(value)) return 1;
const clamped = Math.min(UI_SCALE_MAX, Math.max(UI_SCALE_MIN, value));
// snap to the step grid; toFixed strips float noise (0.7000000000000001 -> 0.7)
return Number((Math.round(clamped / UI_SCALE_STEP) * UI_SCALE_STEP).toFixed(2));
}
export function effectiveConcurrency(settings: AppSettings): number {
if (settings.transferMode === "single") return 1;
return Math.max(1, Math.min(8, Math.round(settings.transferConcurrency) || 1));
}
export type TransferState =
| "queued"
| "scanning"
| "running"
| "completed"
| "failed"
| "cancelled";
export interface TransferSnapshot {
id: string;
name: string; // display name (basename of source)
srcSessionId: string;
srcPath: string;
dstSessionId: string;
dstDir: string;
state: TransferState;
totalFiles: number;
totalBytes: number;
doneFiles: number;
doneBytes: number;
currentFile: string;
currentFileBytes: number;
currentFileTotal: number;
error?: string;
}