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:
@@ -1,8 +1,11 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { DEFAULT_SETTINGS, type AppSettings } from "../../shared/types";
|
||||
import { clampUiScale, DEFAULT_SETTINGS, type AppSettings } from "../../shared/types";
|
||||
import { appDataDir } from "./paths";
|
||||
|
||||
/** The retired 3-tier axis, mapped onto the scale that replaced it. */
|
||||
const LEGACY_TEXT_SIZE: Record<string, number> = { compact: 0.9, default: 1, large: 1.2 };
|
||||
|
||||
/** App settings persisted bun-side (WKWebView localStorage on views:// is unreliable). */
|
||||
export class SettingsStore {
|
||||
private readonly file = path.join(appDataDir(), "settings.json");
|
||||
@@ -10,7 +13,18 @@ export class SettingsStore {
|
||||
async load(): Promise<AppSettings> {
|
||||
try {
|
||||
const data = JSON.parse(await fs.readFile(this.file, "utf8"));
|
||||
return { ...DEFAULT_SETTINGS, ...data };
|
||||
const settings: AppSettings = { ...DEFAULT_SETTINGS, ...data };
|
||||
// migrate the retired textSize tier into the scale that replaced it —
|
||||
// but never over an explicit uiScale (a file carrying both has already
|
||||
// been migrated once and then adjusted)
|
||||
if (data.uiScale === undefined && typeof data.textSize === "string") {
|
||||
settings.uiScale = LEGACY_TEXT_SIZE[data.textSize] ?? 1;
|
||||
}
|
||||
// clamp ALWAYS, not just on migration: buildPreload writes this value
|
||||
// into every window's root font-size, so a hand-edited settings.json
|
||||
// (uiScale: 100, "2", NaN) must not be able to render the UI unusable
|
||||
settings.uiScale = clampUiScale(Number(settings.uiScale));
|
||||
return settings;
|
||||
} catch {
|
||||
return { ...DEFAULT_SETTINGS };
|
||||
}
|
||||
|
||||
+6
-1
@@ -59,7 +59,12 @@ export function buildPreload(
|
||||
if (settings.theme !== "system") dataset["theme"] = settings.theme;
|
||||
if (settings.darkTone !== "dark") dataset["darkTone"] = settings.darkTone;
|
||||
if (settings.accent !== "graphite") dataset["accent"] = settings.accent;
|
||||
if (settings.textSize !== "default") dataset["text"] = settings.textSize;
|
||||
// interface scale: mirrors SettingsService.applyUiScale so the FIRST paint
|
||||
// is already scaled — without this every window opens at 100% and snaps.
|
||||
// Written as an inline style, not a data attribute: the scale is continuous.
|
||||
if (settings.uiScale !== 1) {
|
||||
script += `document.documentElement.style.fontSize=${JSON.stringify(`${16 * settings.uiScale}px`)};`;
|
||||
}
|
||||
for (const [key, value] of Object.entries(dataset)) {
|
||||
script += `document.documentElement.dataset[${JSON.stringify(key)}]=${JSON.stringify(value)};`;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: "同時数",
|
||||
|
||||
@@ -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
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user