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>
This commit is contained in:
2026-08-16 18:09:59 +02:00
co-authored by Claude
parent 9f2a076e08
commit f101257f20
14 changed files with 131 additions and 55 deletions
+1 -1
View File
@@ -53,7 +53,7 @@ export const de = {
teal: "Petrol",
},
fileIcons: { label: "Datei-Symbole", plain: "Einfach", multi: "Farbig" },
interface: { label: "Oberfläche", compact: "Kompakt", default: "Normal", large: "Groß" },
interface: { label: "Oberfläche", smaller: "Kleiner", larger: "Größer", reset: "Auf 100% zurücksetzen" },
speedUnit: { label: "Geschwindigkeitseinheit" },
transfers: { label: "Übertragungen", single: "Einzeln", multi: "Mehrere" },
atOnce: "Gleichzeitig",
+1 -1
View File
@@ -57,7 +57,7 @@ export const en = {
teal: "Teal",
},
fileIcons: { label: "File icons", plain: "Plain", multi: "Multi" },
interface: { label: "Interface", compact: "Compact", default: "Default", large: "Large" },
interface: { label: "Interface", smaller: "Smaller", larger: "Larger", reset: "Reset to 100%" },
speedUnit: { label: "Speed unit" },
transfers: { label: "Transfers", single: "Single", multi: "Multi" },
atOnce: "At once",
+1 -1
View File
@@ -53,7 +53,7 @@ export const es = {
teal: "Turquesa",
},
fileIcons: { label: "Iconos", plain: "Simple", multi: "Color" },
interface: { label: "Interfaz", compact: "Compacta", default: "Normal", large: "Grande" },
interface: { label: "Interfaz", smaller: "Más pequeña", larger: "Más grande", reset: "Restablecer al 100%" },
speedUnit: { label: "Unidad de velocidad" },
transfers: { label: "Transferencias", single: "Una", multi: "Varias" },
atOnce: "A la vez",
+1 -1
View File
@@ -55,7 +55,7 @@ export const eu = {
teal: "Turkesa",
},
fileIcons: { label: "Fitxategi-ikonoak", plain: "Soila", multi: "Koloretsua" },
interface: { label: "Interfazea", compact: "Trinkoa", default: "Lehenetsia", large: "Handia" },
interface: { label: "Interfazea", smaller: "Txikiagoa", larger: "Handiagoa", reset: "Berrezarri %100era" },
speedUnit: { label: "Abiadura-unitatea" },
transfers: { label: "Transferentziak", single: "Banaka", multi: "Anitz" },
atOnce: "Aldi berean",
+1 -1
View File
@@ -53,7 +53,7 @@ export const fr = {
teal: "Sarcelle",
},
fileIcons: { label: "Icônes", plain: "Simple", multi: "Couleur" },
interface: { label: "Interface", compact: "Compacte", default: "Normale", large: "Grande" },
interface: { label: "Interface", smaller: "Plus petite", larger: "Plus grande", reset: "Réinitialiser à 100%" },
speedUnit: { label: "Unité de vitesse" },
transfers: { label: "Transferts", single: "Un", multi: "Plusieurs" },
atOnce: "Simultanés",
+1 -1
View File
@@ -54,7 +54,7 @@ export const ja = {
teal: "ティール",
},
fileIcons: { label: "ファイルアイコン", plain: "シンプル", multi: "カラー" },
interface: { label: "インターフェース", compact: "コンパクト", default: "標準", large: "大" },
interface: { label: "インターフェース", smaller: "縮小", larger: "拡大", reset: "100%に戻す" },
speedUnit: { label: "速度の単位" },
transfers: { label: "転送", single: "1件ずつ", multi: "複数" },
atOnce: "同時数",
+1 -1
View File
@@ -54,7 +54,7 @@ export const zh = {
teal: "青色",
},
fileIcons: { label: "文件图标", plain: "简约", multi: "彩色" },
interface: { label: "界面", compact: "紧凑", default: "默认", large: "大" },
interface: { label: "界面", smaller: "缩小", larger: "放大", reset: "重置为100%" },
speedUnit: { label: "速度单位" },
transfers: { label: "传输", single: "单个", multi: "多个" },
atOnce: "同时",
+25 -3
View File
@@ -100,8 +100,13 @@ export interface AppSettings {
/** Visible optional file-list columns (both panels; Name always shows). */
columns: ColumnKey[];
speedUnit: "MBps" | "Mbps";
/** Size of the panel interface: file list, path bar, header controls. */
textSize: "compact" | "default" | "large";
/**
* 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". */
@@ -127,7 +132,7 @@ export const DEFAULT_SETTINGS: AppSettings = {
iconColors: "multi",
columns: ["size", "mtime"],
speedUnit: "MBps",
textSize: "default",
uiScale: 1,
transferMode: "single",
transferConcurrency: 3,
autoClearCompleted: true,
@@ -136,6 +141,23 @@ export const DEFAULT_SETTINGS: AppSettings = {
// 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));