mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
fix(electron): own app zoom and migrate legacy settings
Problem: Electron inherited the service-owned zoom value and hid app zoom on non-Windows platforms.\n\nRoot cause: the shell trusted geometry.zoomFactor and the renderer used legacy Wails bindings.\n\nFix: add an Electron-owned zoom store with safe legacy migration, trusted IPC, runtime updates, menu shortcuts, and cross-platform settings visibility. Keep browser tab zoom independent.\n\nVerification: Electron typecheck and 103 Electron tests pass; frontend typecheck and production build pass.
This commit is contained in:
1 parent
c4add668b5
commit
0683e05ce9
16 files changed
+176
-12
No files matched your search
@@ -28,6 +28,7 @@ import { ServiceSupervisor } from "./service.js";
|
||||
import { claimShellInstance } from "./singleInstance.js";
|
||||
import { TrayHost } from "./tray.js";
|
||||
import { DEFAULT_GEOMETRY, MainWindow } from "./window.js";
|
||||
import { AppZoomStore } from "./zoomStore.js";
|
||||
|
||||
const MAIN_WINDOW_PERMISSIONS = new Set(["clipboard-read", "clipboard-sanitized-write", "fullscreen", "notifications"]);
|
||||
const TAKEOVER_KINDS = new Set<string>(["mousedown", "keydown", "wheel", "touchstart", "pointerdown"]);
|
||||
@@ -71,6 +72,7 @@ function bootstrap(dataHome: string): void {
|
||||
const distRoot = resolveDistRoot({ env: process.env, appPath: app.getAppPath(), resourcesPath: process.resourcesPath, packaged: app.isPackaged });
|
||||
const devURL = (process.env.REASONIX_ELECTRON_DEV_URL ?? "").trim();
|
||||
const appURL = devURL !== "" ? devURL : APP_INDEX_URL;
|
||||
const zoomStore = new AppZoomStore(join(dataHome, "electron-app-zoom.json"), join(dataHome, "desktop-zoom.json"));
|
||||
const icons = iconCandidates({ platform: process.platform, appPath: app.getAppPath(), resourcesPath: process.resourcesPath, packaged: app.isPackaged });
|
||||
const windowIcon = process.platform === "darwin" ? undefined : (firstExisting(icons.window) ?? undefined);
|
||||
const serviceBinary = (process.env.REASONIX_DESKTOP_SERVICE ?? "").trim()
|
||||
@@ -110,6 +112,7 @@ function bootstrap(dataHome: string): void {
|
||||
else if (action === "restart") void service.restart().catch(() => undefined);
|
||||
else lifecycle.approve();
|
||||
},
|
||||
zoomStore,
|
||||
});
|
||||
|
||||
const downloads = new DownloadTracker({
|
||||
@@ -244,9 +247,17 @@ function bootstrap(dataHome: string): void {
|
||||
documents.clear();
|
||||
}
|
||||
},
|
||||
onReady: (hello: HelloResult) => {
|
||||
onReady: async (hello: HelloResult) => {
|
||||
log.info(`desktop service ready: generation ${hello.runtimeGeneration}, pid ${hello.service.pid}`);
|
||||
if (!mainWindow.browserWindow) mainWindow.create(hello.window);
|
||||
if (!mainWindow.browserWindow) {
|
||||
try {
|
||||
await zoomStore.load();
|
||||
mainWindow.create(hello.window);
|
||||
} catch (error) {
|
||||
log.warn(`app zoom initialization failed: ${errorText(error)}`);
|
||||
mainWindow.create(DEFAULT_GEOMETRY);
|
||||
}
|
||||
}
|
||||
// Reattach the surviving renderer after a service restart. Reloading
|
||||
// would destroy unsent composer drafts; desktop:resync repairs reads.
|
||||
if (!mainWindow.reattachApp()) void mainWindow.loadApp();
|
||||
@@ -328,6 +339,9 @@ function bootstrap(dataHome: string): void {
|
||||
toggleDevTools: () => mainWindow.toggleDevTools(),
|
||||
showWindow: () => mainWindow.show("menu"),
|
||||
quit: () => lifecycle.requestQuit(),
|
||||
zoomIn: () => { void mainWindow.stepAppZoom(1); },
|
||||
zoomOut: () => { void mainWindow.stepAppZoom(-1); },
|
||||
resetZoom: () => { void mainWindow.resetAppZoom(); },
|
||||
});
|
||||
log.info(`shell starting: service ${serviceBinary}, ui ${appURL}, dist ${distRoot}, home ${dataHome}`);
|
||||
return service.start().catch(() => undefined);
|
||||
|
||||
@@ -31,7 +31,8 @@ test("trusted toolbar takeover bypasses input suppression immediately, fences ac
|
||||
registerRendererIpc({ ipcMain, contract: parseContract({ digest: "test", commands: ["ListTabs"] }),
|
||||
window: { isTrustedSender: (sender, frame) => sender === trustedSender && frame === trustedFrame,
|
||||
minimise() {}, toggleMaximise() {}, isMaximised: () => false, close() {},
|
||||
bounds: () => ({ x: 0, y: 0, width: 1, height: 1, maximised: false }), setTheme() {}, setBackgroundColour() {} },
|
||||
bounds: () => ({ x: 0, y: 0, width: 1, height: 1, maximised: false }), setTheme() {}, setBackgroundColour() {},
|
||||
getAppZoom: async () => 1, setAppZoom: async () => 1, resetAppZoom: async () => 1 },
|
||||
invoke: async () => undefined, serviceState: () => ({ phase: "ready", generation: "g" }),
|
||||
clipboard: { writeText() {}, readText: () => "" }, openExternal: async () => {}, log: silentLog,
|
||||
browser: { list: () => manager.list(), open: async (url, options) => { const opened = await manager.open(url, options); return manager.list().find((entry) => entry.id === opened.id)!; },
|
||||
|
||||
@@ -45,6 +45,9 @@ test("renderer invokes are gated by sender identity and the contract allowlist",
|
||||
bounds: () => ({ x: 1, y: 2, width: 3, height: 4, maximised: false }),
|
||||
setTheme() {},
|
||||
setBackgroundColour() {},
|
||||
getAppZoom: async () => 1,
|
||||
setAppZoom: async () => 1,
|
||||
resetAppZoom: async () => 1,
|
||||
},
|
||||
serviceState: () => ({ phase: "ready" as const, generation: "g-test" }),
|
||||
invoke: async (method, args) => {
|
||||
|
||||
@@ -24,6 +24,9 @@ export interface RendererWindowApi {
|
||||
bounds(): WindowBounds;
|
||||
setTheme(theme: WindowTheme): void;
|
||||
setBackgroundColour(r: number, g: number, b: number, a: number): void;
|
||||
getAppZoom(): Promise<number>;
|
||||
setAppZoom(factor: number): Promise<number>;
|
||||
resetAppZoom(): Promise<number>;
|
||||
}
|
||||
|
||||
// The user-driven browser panel: no grant is involved because the user is
|
||||
@@ -126,6 +129,9 @@ export function registerRendererIpc(deps: RendererIpcDeps): void {
|
||||
handle(IPC.windowGetBounds, () => deps.window.bounds());
|
||||
handle(IPC.windowSetTheme, (theme) => deps.window.setTheme(theme === "light" || theme === "dark" ? theme : "system"));
|
||||
handle(IPC.windowSetBackground, (r, g, b, a) => deps.window.setBackgroundColour(finite(r), finite(g), finite(b), finite(a, 255)));
|
||||
handle(IPC.appZoomGet, () => deps.window.getAppZoom());
|
||||
handle(IPC.appZoomSet, (factor) => deps.window.setAppZoom(finite(factor, Number.NaN)));
|
||||
handle(IPC.appZoomReset, () => deps.window.resetAppZoom());
|
||||
|
||||
const browser = deps.browser;
|
||||
if (!browser) return;
|
||||
|
||||
@@ -6,6 +6,9 @@ export interface MenuDeps {
|
||||
toggleDevTools(): void;
|
||||
showWindow(): void;
|
||||
quit(): void;
|
||||
zoomIn(): void;
|
||||
zoomOut(): void;
|
||||
resetZoom(): void;
|
||||
}
|
||||
|
||||
export function applicationMenuTemplate(deps: MenuDeps): MenuItemConstructorOptions[] {
|
||||
@@ -22,6 +25,11 @@ export function applicationMenuTemplate(deps: MenuDeps): MenuItemConstructorOpti
|
||||
],
|
||||
},
|
||||
{ role: "editMenu" },
|
||||
{ label: "View", submenu: [
|
||||
{ label: "Zoom In", accelerator: "CmdOrCtrl+=", click: () => deps.zoomIn() },
|
||||
{ label: "Zoom Out", accelerator: "CmdOrCtrl+-", click: () => deps.zoomOut() },
|
||||
{ label: "Reset Zoom", accelerator: "CmdOrCtrl+0", click: () => deps.resetZoom() },
|
||||
] },
|
||||
{ role: "windowMenu" },
|
||||
];
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { shellActionFromURL, type ShellAction } from "./failurePage.js";
|
||||
import type { HelloWindow } from "./handshake.js";
|
||||
import { errorText, type Logger } from "./log.js";
|
||||
import { APP_ORIGIN } from "./protocol.js";
|
||||
import { AppZoomStore } from "./zoomStore.js";
|
||||
|
||||
export const DEFAULT_GEOMETRY: HelloWindow = { width: 1280, height: 820, minWidth: 760, minHeight: 480, frameless: false, zoomFactor: 1 };
|
||||
|
||||
@@ -19,6 +20,7 @@ export interface MainWindowDeps {
|
||||
onCloseRequested(): Promise<boolean>;
|
||||
onCloseAllowed(): void;
|
||||
onShellAction(action: ShellAction): void;
|
||||
zoomStore: AppZoomStore;
|
||||
}
|
||||
|
||||
type Content = "none" | "app" | "failure";
|
||||
@@ -70,7 +72,7 @@ export class MainWindow {
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
spellcheck: false,
|
||||
zoomFactor: geometry.zoomFactor,
|
||||
zoomFactor: this.deps.zoomStore.current.appZoomFactor,
|
||||
},
|
||||
});
|
||||
this.win = win;
|
||||
@@ -109,6 +111,18 @@ export class MainWindow {
|
||||
});
|
||||
}
|
||||
|
||||
async getAppZoom(): Promise<number> { return (await this.deps.zoomStore.load()).appZoomFactor; }
|
||||
async setAppZoom(factor: number): Promise<number> {
|
||||
const state = await this.deps.zoomStore.set(factor);
|
||||
this.browserWindow?.webContents.setZoomFactor(state.appZoomFactor);
|
||||
return state.appZoomFactor;
|
||||
}
|
||||
async resetAppZoom(): Promise<number> { return this.setAppZoom(1); }
|
||||
async stepAppZoom(direction: 1 | -1): Promise<number> {
|
||||
const current = await this.getAppZoom();
|
||||
return this.setAppZoom(current + direction * 0.05);
|
||||
}
|
||||
|
||||
async loadApp(): Promise<void> {
|
||||
const win = this.browserWindow;
|
||||
if (!win) return;
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtemp, writeFile, readFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { AppZoomStore, normalizeAppZoom } from "./zoomStore.js";
|
||||
|
||||
test("normalizes app zoom and defaults safely", () => {
|
||||
assert.equal(normalizeAppZoom(Number.NaN), 1);
|
||||
assert.equal(normalizeAppZoom(0.71), 0.7);
|
||||
assert.equal(normalizeAppZoom(3), 2);
|
||||
});
|
||||
|
||||
test("migrates only explicitly user-owned legacy zoom", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "reasonix-zoom-"));
|
||||
const legacy = join(root, "desktop-zoom.json");
|
||||
await writeFile(legacy, JSON.stringify({ zoomFactor: 0.7 }));
|
||||
const store = new AppZoomStore(join(root, "electron-app-zoom.json"), legacy);
|
||||
assert.equal((await store.load()).appZoomFactor, 1);
|
||||
await writeFile(legacy, JSON.stringify({ zoomFactor: 0.7, source: "user" }));
|
||||
const second = new AppZoomStore(join(root, "electron-app-zoom-2.json"), legacy);
|
||||
assert.equal((await second.load()).appZoomFactor, 0.7);
|
||||
});
|
||||
|
||||
test("persists user value and reset", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "reasonix-zoom-"));
|
||||
const file = join(root, "nested", "zoom.json");
|
||||
const store = new AppZoomStore(file);
|
||||
await store.set(0.85);
|
||||
assert.equal((await store.load()).appZoomFactor, 0.85);
|
||||
await store.reset();
|
||||
assert.deepEqual(JSON.parse(await readFile(file, "utf8")), { version: 1, appZoomFactor: 1, source: "user" });
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import { rename, readFile, writeFile, mkdir } from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
export const MIN_APP_ZOOM = 0.5;
|
||||
export const MAX_APP_ZOOM = 2;
|
||||
export const APP_ZOOM_STEP = 0.05;
|
||||
export type AppZoomSource = "default" | "user" | "migrated";
|
||||
export interface AppZoomState { version: 1; appZoomFactor: number; source: AppZoomSource; }
|
||||
|
||||
const DEFAULT: AppZoomState = { version: 1, appZoomFactor: 1, source: "default" };
|
||||
|
||||
export function normalizeAppZoom(value: number): number {
|
||||
if (!Number.isFinite(value)) return 1;
|
||||
const clamped = Math.min(MAX_APP_ZOOM, Math.max(MIN_APP_ZOOM, value));
|
||||
return Number((MIN_APP_ZOOM + Math.round((clamped - MIN_APP_ZOOM) / APP_ZOOM_STEP) * APP_ZOOM_STEP).toFixed(2));
|
||||
}
|
||||
|
||||
export class AppZoomStore {
|
||||
private state: AppZoomState = DEFAULT;
|
||||
constructor(private readonly file: string, private readonly legacyFile?: string) {}
|
||||
async load(): Promise<AppZoomState> {
|
||||
try {
|
||||
const parsed = JSON.parse(await readFile(this.file, "utf8")) as Partial<AppZoomState>;
|
||||
if (parsed.version === 1 && typeof parsed.appZoomFactor === "number" && Number.isFinite(parsed.appZoomFactor)
|
||||
&& (parsed.source === "default" || parsed.source === "user" || parsed.source === "migrated")) {
|
||||
this.state = { version: 1, appZoomFactor: normalizeAppZoom(parsed.appZoomFactor), source: parsed.source };
|
||||
return this.state;
|
||||
}
|
||||
} catch { /* first launch or corrupt file */ }
|
||||
this.state = await this.migrate();
|
||||
await this.persist(this.state);
|
||||
return this.state;
|
||||
}
|
||||
async set(factor: number, source: AppZoomSource = "user"): Promise<AppZoomState> {
|
||||
if (!Number.isFinite(factor)) throw new Error("app zoom factor must be finite");
|
||||
this.state = { version: 1, appZoomFactor: normalizeAppZoom(factor), source };
|
||||
await this.persist(this.state);
|
||||
return this.state;
|
||||
}
|
||||
async reset(): Promise<AppZoomState> { return this.set(1, "user"); }
|
||||
get current(): AppZoomState { return this.state; }
|
||||
private async migrate(): Promise<AppZoomState> {
|
||||
if (!this.legacyFile) return DEFAULT;
|
||||
try {
|
||||
const parsed = JSON.parse(await readFile(this.legacyFile, "utf8")) as { zoomFactor?: unknown; source?: unknown };
|
||||
// Legacy files had no trustworthy source marker; do not inherit an accidental 70% value.
|
||||
if (parsed.source === "user" && typeof parsed.zoomFactor === "number" && Number.isFinite(parsed.zoomFactor)) {
|
||||
return { version: 1, appZoomFactor: normalizeAppZoom(parsed.zoomFactor), source: "migrated" };
|
||||
}
|
||||
} catch { /* use default */ }
|
||||
return DEFAULT;
|
||||
}
|
||||
private async persist(state: AppZoomState): Promise<void> {
|
||||
await mkdir(dirname(this.file), { recursive: true });
|
||||
const temp = join(dirname(this.file), `.${state.version}.zoom.tmp`);
|
||||
await writeFile(temp, `${JSON.stringify(state)}\n`, "utf8");
|
||||
await rename(temp, this.file);
|
||||
}
|
||||
}
|
||||
@@ -200,6 +200,9 @@ contextBridge.exposeInMainWorld("reasonixDesktop", {
|
||||
minimise: () => fire(IPC.windowMinimise),
|
||||
toggleMaximise: () => fire(IPC.windowToggleMaximise),
|
||||
close: () => fire(IPC.windowClose),
|
||||
getAppZoom: () => call(IPC.appZoomGet),
|
||||
setAppZoom: (factor: number) => call(IPC.appZoomSet, factor),
|
||||
resetAppZoom: () => call(IPC.appZoomReset),
|
||||
},
|
||||
getPathForFile: (file: File) => webUtils.getPathForFile(file),
|
||||
onServiceState,
|
||||
|
||||
@@ -14,6 +14,9 @@ export const IPC = {
|
||||
windowGetBounds: "reasonix:native:window-get-bounds",
|
||||
windowSetTheme: "reasonix:native:window-set-theme",
|
||||
windowSetBackground: "reasonix:native:window-set-background",
|
||||
appZoomGet: "reasonix:native:app-zoom-get",
|
||||
appZoomSet: "reasonix:native:app-zoom-set",
|
||||
appZoomReset: "reasonix:native:app-zoom-reset",
|
||||
browserList: "reasonix:browser:list",
|
||||
browserOpen: "reasonix:browser:open",
|
||||
browserClose: "reasonix:browser:close",
|
||||
|
||||
@@ -78,7 +78,10 @@ export function installDesktopHostStub(commands: object, options: DesktopHostStu
|
||||
isMaximised: () => Promise.resolve(false),
|
||||
minimise: () => {},
|
||||
toggleMaximise: () => {},
|
||||
close: () => {},
|
||||
close: () => {},
|
||||
getAppZoom: async () => 1,
|
||||
setAppZoom: async () => 1,
|
||||
resetAppZoom: async () => 1,
|
||||
},
|
||||
getPathForFile: options.getPathForFile ?? (() => ""),
|
||||
onServiceState: () => () => {},
|
||||
|
||||
@@ -245,11 +245,14 @@ export function SettingsPanel({
|
||||
setConversationWidth(applyConversationWidth(s.conversationWidth));
|
||||
}, [s?.conversationWidth, s?.desktopTheme, s?.desktopThemeStyle, s?.desktopTerminalTheme]);
|
||||
useEffect(() => {
|
||||
if (desktopPlatform !== "windows") return;
|
||||
const host = desktopHost();
|
||||
if (host.kind === "none" && desktopPlatform !== "windows") return;
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
try {
|
||||
const persisted = await app.GetDesktopZoomFactor();
|
||||
const persisted = host.kind === "electron"
|
||||
? await host.native.getAppZoom()
|
||||
: await app.GetDesktopZoomFactor();
|
||||
if (cancelled || typeof persisted !== "number" || !Number.isFinite(persisted)) return;
|
||||
const snapped = snapZoom(persisted);
|
||||
saveRestartZoom(snapped);
|
||||
@@ -367,7 +370,9 @@ export function SettingsPanel({
|
||||
setWarning(null);
|
||||
setZoomPct(zoomToPercent(snapped));
|
||||
try {
|
||||
await app.SetDesktopZoomFactor(snapped);
|
||||
const host = desktopHost();
|
||||
if (host.kind === "electron") await host.native.setAppZoom(snapped);
|
||||
else await app.SetDesktopZoomFactor(snapped);
|
||||
if (seq === zoomSaveSeq.current) saveRestartZoom(snapped);
|
||||
} catch (e) {
|
||||
if (seq !== zoomSaveSeq.current) return;
|
||||
@@ -443,7 +448,7 @@ export function SettingsPanel({
|
||||
terminalTheme={terminalTheme}
|
||||
conversationWidth={conversationWidth}
|
||||
textSize={textSize}
|
||||
showDisplayZoom={desktopPlatform === "windows"}
|
||||
showDisplayZoom={desktopHost().kind !== "none" || desktopPlatform === "windows"}
|
||||
zoomPct={zoomPct}
|
||||
fontFamily={fontFamily}
|
||||
monoFontFamily={monoFontFamily}
|
||||
|
||||
@@ -39,6 +39,9 @@ export interface ReasonixDesktopHost {
|
||||
minimise(): void;
|
||||
toggleMaximise(): void;
|
||||
close(): void;
|
||||
getAppZoom(): Promise<number>;
|
||||
setAppZoom(factor: number): Promise<number>;
|
||||
resetAppZoom(): Promise<number>;
|
||||
};
|
||||
getPathForFile(file: File): string;
|
||||
onServiceState(cb: (state: ServiceState) => void): () => void;
|
||||
@@ -63,6 +66,9 @@ export interface DesktopHost {
|
||||
setWindowTheme(theme: WindowTheme): void;
|
||||
setWindowBackground(r: number, g: number, b: number, a: number): void;
|
||||
getWindowBounds(): Promise<WindowBounds> | undefined;
|
||||
getAppZoom(): Promise<number>;
|
||||
setAppZoom(factor: number): Promise<number>;
|
||||
resetAppZoom(): Promise<number>;
|
||||
onFilesDropped(cb: (paths: string[]) => void): () => void;
|
||||
getPathForFile?(file: File): string;
|
||||
onServiceState(cb: (state: ServiceState) => void): () => void;
|
||||
@@ -97,6 +103,9 @@ const serverHost: DesktopHost = {
|
||||
getWindowBounds: () => undefined,
|
||||
onFilesDropped: () => noop,
|
||||
onServiceState: () => noop,
|
||||
getAppZoom: async () => 1,
|
||||
setAppZoom: async () => 1,
|
||||
resetAppZoom: async () => 1,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -150,6 +159,9 @@ const electronHostFrom = (host: ReasonixDesktopHost): DesktopHost => {
|
||||
setWindowTheme: (theme) => host.native.window.setTheme(theme),
|
||||
setWindowBackground: (r, g, b, a) => host.native.window.setBackgroundColour(r, g, b, a),
|
||||
getWindowBounds: () => host.native.window.getBounds(),
|
||||
getAppZoom: () => host.native.window.getAppZoom(),
|
||||
setAppZoom: (factor) => host.native.window.setAppZoom(factor),
|
||||
resetAppZoom: () => host.native.window.resetAppZoom(),
|
||||
onFilesDropped: (cb) => {
|
||||
installElectronDropHandlers();
|
||||
dropListeners.add(cb);
|
||||
|
||||
@@ -2770,7 +2770,7 @@ export const en = {
|
||||
"settings.textSizeLarge": "Large",
|
||||
"settings.textSizeXLarge": "Extra large",
|
||||
"settings.textSizeXXLarge": "Huge",
|
||||
"settings.displayZoom": "Display zoom (restart required)",
|
||||
"settings.displayZoom": "Display zoom (applies immediately)",
|
||||
"settings.displayZoomDecrease": "Decrease display zoom",
|
||||
"settings.displayZoomIncrease": "Increase display zoom",
|
||||
"settings.displayZoomReset": "Reset display zoom to 100%",
|
||||
|
||||
@@ -1892,7 +1892,7 @@ export const zhTW: Record<DictKey, string> = {
|
||||
"settings.textSizeLarge": "大",
|
||||
"settings.textSizeXLarge": "特大",
|
||||
"settings.textSizeXXLarge": "超大",
|
||||
"settings.displayZoom": "顯示縮放(需重啟生效)",
|
||||
"settings.displayZoom": "顯示縮放(立即生效)",
|
||||
"settings.displayZoomDecrease": "縮小顯示縮放",
|
||||
"settings.displayZoomIncrease": "放大顯示縮放",
|
||||
"settings.displayZoomReset": "恢復顯示縮放到 100%",
|
||||
|
||||
@@ -2773,7 +2773,7 @@ export const zh: Record<DictKey, string> = {
|
||||
"settings.textSizeLarge": "大",
|
||||
"settings.textSizeXLarge": "特大",
|
||||
"settings.textSizeXXLarge": "超大",
|
||||
"settings.displayZoom": "显示缩放(需重启生效)",
|
||||
"settings.displayZoom": "显示缩放(立即生效)",
|
||||
"settings.displayZoomDecrease": "缩小显示缩放",
|
||||
"settings.displayZoomIncrease": "放大显示缩放",
|
||||
"settings.displayZoomReset": "恢复显示缩放到 100%",
|
||||
|
||||
Reference in new issue
Block a user