From 141d675bf7f4dbd3ef89a0b526700583fb8bf927 Mon Sep 17 00:00:00 2001 From: Jokin Date: Mon, 5 Oct 2026 09:55:15 +0200 Subject: [PATCH] Move settings into a standalone Settings window The tab-bar popover had outgrown itself. Settings now open in their own native window (a fourth window kind, built like the connection manager but not modal): a sidebar with Appearance, Transfers, Editor, File list and About, holding every popover row plus the visible-columns choice. Each control is an ordinary settings save, so the other windows follow live while it stays open. The gear button in the tab bar, Ctrl+, and the macOS app menu's "Settings..." (Cmd+,) open it. On macOS Cmd+W arrives as the main window's close-tab menu action, so bun closes the focused Settings window first instead of a main-window tab. Co-Authored-By: Claude --- README.md | 4 +- docs/architecture.md | 42 ++- docs/build-and-release.md | 2 +- docs/gotchas.md | 5 + docs/platform-notes.md | 6 +- src/bun/index.ts | 42 ++- src/bun/menu.ts | 2 + src/bun/rpc-handlers.ts | 76 ++-- src/bun/settings-window.ts | 91 +++++ src/bun/windows.ts | 4 +- src/shared/i18n/de.ts | 13 +- src/shared/i18n/en.ts | 13 +- src/shared/i18n/es.ts | 13 +- src/shared/i18n/eu.ts | 13 +- src/shared/i18n/fr.ts | 13 +- src/shared/i18n/ja.ts | 13 +- src/shared/i18n/zh.ts | 13 +- src/shared/rpc.ts | 9 +- ui/src/app/core/context-menu.service.ts | 2 +- ui/src/app/core/icons.ts | 2 - ui/src/app/core/platform.ts | 4 +- ui/src/app/core/shortcuts.service.ts | 13 +- ui/src/app/skins/shadlike/app.ts | 3 + ui/src/app/skins/shadlike/index.ts | 4 +- .../settings-window/settings-window.html | 303 ++++++++++++++++ .../settings-window/settings-window.ts | 105 ++++++ ui/src/app/skins/shadlike/tab-bar/tab-bar.ts | 332 +----------------- ui/src/main.ts | 6 +- 28 files changed, 743 insertions(+), 405 deletions(-) create mode 100644 src/bun/settings-window.ts create mode 100644 ui/src/app/skins/shadlike/settings-window/settings-window.html create mode 100644 ui/src/app/skins/shadlike/settings-window/settings-window.ts diff --git a/README.md b/README.md index 317f12d..fa7ede5 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ Dense reference material lives in [docs/](docs/): [architecture](docs/architectu - **Menu-bar (tray) icon**: template icon that adapts to light/dark; click it to bring the app to the front. - **Edit files** (F4 / toolbar / File menu): a built-in editor works on local - and remote files directly; or pick an external app (Options → Editor → + and remote files directly; or pick an external app (Settings → Editor → Custom → Choose…). External edits of remote files download to a temp folder, are watched, and every save auto-uploads (visible in the transfer list) until the session disconnects. Leftover temp files from a crash are offered for @@ -181,7 +181,7 @@ Saved connections live in a plain JSON file in the OS app-data directory `Space` select · `Enter` open folder · `Backspace` parent · `Ctrl/Cmd+A` select all · `F5` send to other panel · `F2` rename · `F7` new folder · `F8`/`Delete` delete · `Cmd/Ctrl+T` new tab · -`Cmd/Ctrl+W` close tab · `Ctrl+Tab` next tab · +`Cmd/Ctrl+W` close tab · `Ctrl+Tab` next tab · `Cmd/Ctrl+,` settings · `Cmd/Ctrl+=`/`-`/`0` interface bigger / smaller / 100%. (On Macs, function keys may need `fn` — every action also has a toolbar button.) diff --git a/docs/architecture.md b/docs/architecture.md index d6039d9..9a8ae27 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -30,8 +30,8 @@ Each `BrowserWindow` MUST receive its own `defineRPC(...)` instance: binding a BrowserView calls `rpc.setTransport(...)`, so a shared instance would have its transport stolen by the most recent window. `createAppRPC` (main), `createEditorRPC` (per editor window, hooks close over that window's record), -`createManagerRPC` (the single manager window). `rpc.send.*` on an instance -targets exactly its own webview. +`createManagerRPC` (the single manager window), `createSettingsRPC` (the single +Settings window). `rpc.send.*` on an instance targets exactly its own webview. ### The `events` late-binding pattern @@ -39,15 +39,15 @@ targets exactly its own webview. exist until after construction). `AppDeps.events` is a mutable object of no-op functions, reassigned in `src/bun/index.ts` after everything exists: `sessionClosed`, `connectionsChanged`, `openEditor`, `settingsChanged`, -`openConnectionManager`, `showAbout`. +`openConnectionManager`, `openSettings`, `showAbout`. ## Window kinds `src/bun/windows.ts` — `buildPreload(mode, settings, ctx?)` injects -`__APP_PLATFORM`, `__APP_MODE` (`main|editor|manager`), `__APP_CTX` before any -page script, plus the persisted theme attributes and interface scale so the -first paint is already right. `ui/src/main.ts` picks the root component from -`APP_MODE`; all three roots use selector `app-root` (index.html hosts only that +`__APP_PLATFORM`, `__APP_MODE` (`main|editor|manager|settings`), `__APP_CTX` +before any page script, plus the persisted theme attributes and interface +scale so the first paint is already right. `ui/src/main.ts` picks the root component from +`APP_MODE`; all four roots use selector `app-root` (index.html hosts only that tag). `chromeOptions()` is the ONLY place the native frame is decided, spread by the main window and `openSecondaryWindow`: macOS `hiddenInset` + `trafficLightOffset {x:12,y:13}` (the UI renders an `h-10 pl-24` drag-region @@ -103,6 +103,20 @@ main's `ConnectionsService` reloads; a reload-retry in `App.findConnection` covers the save→connect race. `setAlwaysOnTop` is deliberately NOT used (it would float over editor windows). +### Settings window (`src/bun/settings-window.ts` + `ui/src/app/skins/shadlike/settings-window/`) + +Single instance, centered over main, **not modal**: every control is a plain +`SettingsService` setter, so each edit is an ordinary save that bun +rebroadcasts — the other windows follow live while it stays open. Opened by +the tab-bar gear and Ctrl+, (`openSettings` from main) and, on macOS, the app +menu's "Settings…" (⌘,, which the menu intercepts first). Its RPC serves only +`getSettings`/`saveSettings`/`pickEditorApp`/`showAbout`. ⌘W reaches bun as +the main window's `close-tab` menu action, so bun checks the Settings window's +focus FIRST and closes it (see gotchas). Sidebar sections: Appearance, +Transfers, Editor, File list (visible columns — the column-header right-click +menu toggles the same setting), About (opens the native About box). The +native title is `options.title`, retitled on a locale switch. + ## Bun modules - `fs/provider.ts` — `FileSystemProvider` interface: uniform local/SFTP ops + @@ -152,8 +166,9 @@ would float over editor windows). atomic tmp+rename, 0600 + win32 `icacls` ACL. `store/settings.ts` — defaults spread-merge on load, so NEW SETTINGS KEYS need no backend change. - `menu.ts` — macOS-only app menu, rebuilt on every connections change (the - Connect submenu mirrors the store). ⌘W is intercepted by the menu, so bun - routes `close-tab` to the focused editor window first. Labels go through + Connect submenu mirrors the store; "Settings…" carries ⌘,). ⌘W is intercepted + by the menu, so bun routes `close-tab` to a focused Settings or editor window + first. Labels go through backend `i18n.t` and the menu is rebuilt on a locale change too. - `i18n.ts` — backend translator (native menus + thrown error messages, which no Angular service can reach). `setLocale(setting)` caches the resolved @@ -175,7 +190,7 @@ would float over editor windows). ui/src/app/ core/ app.config.ts, icons.ts, platform.ts, format.ts, file-icons.ts, metrics.ts and every *.service.ts — the non-visual layer - skins/shadlike/ the component tree: three window roots (SHAD_ROOTS barrel, + skins/shadlike/ the component tree: four window roots (SHAD_ROOTS barrel, picked by main.ts), dialogs, context menu ``` @@ -220,8 +235,9 @@ component; tokens → `ui/src/styles.css`. (`applyTheme/DarkTone/Accent` set `data-*` attributes on ``; `applyUiScale` sets its inline `font-size`); `applyAll` also serves the `settingsChanged` broadcast, so every window follows a save made in any - other. All three window RPCs serve `saveSettings` (editor and manager windows - save too — the scale shortcuts); bun's `settingsSaved()` in index.ts runs the + other. Every window RPC serves `saveSettings` (editor and manager windows + save too — the scale shortcuts — and the Settings window is nothing but + saves); bun's `settingsSaved()` in index.ts runs the side effects once and rebroadcasts to every window **except the sender**, so there is no echo to fight a held Cmd+. `applyAll` never persists. New setting = five touches here + types.ts (backend merges automatically). @@ -316,7 +332,7 @@ pixel-identical to the old neutral hover. **Interface scale** (`settings.uiScale `html { font-size: 16px * scale }`, and since EVERY size in the app is rem (panel tokens like `--row-h`, all Tailwind utilities, ng-icon sizes), the whole UI scales, chrome included. -Driven by the tab-bar stepper, Ctrl/Cmd +/−/0, and preload-baked for a +Driven by the Settings window's stepper, Ctrl/Cmd +/−/0, and preload-baked for a flash-free first paint. It replaced the 3-tier `data-text` axis; the store migrates old `textSize` values. TS that measures rem-sized CSS converts through `@core/metrics` `remPx()` — never an assumed 16. diff --git a/docs/build-and-release.md b/docs/build-and-release.md index 3e4ff79..7d7f975 100644 --- a/docs/build-and-release.md +++ b/docs/build-and-release.md @@ -10,7 +10,7 @@ electrobun}`). It is regenerated by `postinstall` and at the start of every records, hence gitignored. Fresh checkouts fail `tsc` until `bun install` has run once (postinstall creates the file). `BRANDING.version` stays semver-shaped (`0.1.`) for electrobun's config/updater; `BRANDING.buildLabel` -("Build n") is what humans see (About box, options popover). +("Build n") is what humans see (About box, Settings → About). ## Dev loop diff --git a/docs/gotchas.md b/docs/gotchas.md index e7a2fc5..fe00b9d 100644 --- a/docs/gotchas.md +++ b/docs/gotchas.md @@ -114,6 +114,11 @@ touching the adjacent code. app-level `before-quit` supports `event.response = {allow:false}`). All unsaved-state guards must either intercept app-controlled close paths (⌘W, buttons) or survive the close (editor draft mirroring). +- **⌘W in any secondary window arrives as the MAIN window's `close-tab` + menu action** (macOS: the app menu intercepts it before the webview sees + the key). bun must check the focused secondary window first — the Settings + window, then editors — and close THAT; a new window kind that skips this + check makes ⌘W close a main-window tab behind it. - **One rpc instance per window** — BrowserView binding steals the rpc's transport; a shared instance silently sends to the wrong webview. - **RPC before socket open is silently lost.** Everything gates on the diff --git a/docs/platform-notes.md b/docs/platform-notes.md index 92c59af..7c7268d 100644 --- a/docs/platform-notes.md +++ b/docs/platform-notes.md @@ -17,7 +17,7 @@ macOS is the primary target; Windows 11+ x64 and Linux x64 are **beta** targets | Shell wording | `ui context-menu.service.ts` | Reveal in Finder / Move to Trash | Show in Explorer / Move to Recycle Bin | Show in File Manager | | Shortcut labels | `ui platform.ts MOD_LABEL/ALT_LABEL` | ⌘ / ⌥ | Ctrl+ / Alt+ | Ctrl+ / Alt+ | | Tray | `src/bun/tray.ts` | 22 pt template PNG | 16 px colored PNG | colored (needs AppIndicator, see below) | -| App menu | `src/bun/menu.ts` | full menu incl. About | none (early return) — About lives in the options popover | none (unsupported upstream) | +| App menu | `src/bun/menu.ts` | full menu incl. About | none (early return) — Settings opens from the tab-bar gear (Ctrl+,), About from its About section | none (unsupported upstream) | | connections.json protection | `store/connections.ts` | chmod 0600 | `icacls` current-user ACL (0600 is meaningless) | 0600 | | App data dir | `store/paths.ts` | ~/Library/Application Support | %APPDATA% | $XDG_CONFIG_HOME/.config | | ssh-agent | `fs/sftp.ts` | $SSH_AUTH_SOCK | `\\.\pipe\openssh-ssh-agent` fallback | $SSH_AUTH_SOCK | @@ -172,7 +172,7 @@ Code paths written blind (no Windows box was available); verify in order: 7. Ctrl+T/W/F/S and Ctrl+digit tab jumps; tooltips read "Ctrl+…". 8. connections.json ACL: `icacls connections.json` shows only the current user after saving a connection. -9. Editor/manager windows open, modal bounce works, About shows from the - options popover. +9. Editor/manager/Settings windows open, modal bounce works, Ctrl+, opens + Settings and About shows from its About section. 10. Known-issue confirmation: HiDPI blur on a scaled display (expected until upstream fixes it). diff --git a/src/bun/index.ts b/src/bun/index.ts index 728ba21..7b96edc 100644 --- a/src/bun/index.ts +++ b/src/bun/index.ts @@ -10,6 +10,7 @@ import { ManagerWindow } from "./manager-window"; import { setLocale as setBackendLocale } from "./i18n"; import { applyApplicationMenu } from "./menu"; import { createAppRPC } from "./rpc-handlers"; +import { SettingsWindow } from "./settings-window"; import { ConnectionStore } from "./store/connections"; import { migrateLegacyDataDir } from "./store/paths"; import { SettingsStore } from "./store/settings"; @@ -52,6 +53,7 @@ const events = { openEditor: (_ctx: EditorOpenContext) => {}, settingsChanged: (_settings: AppSettings) => {}, openConnectionManager: () => {}, + openSettings: () => {}, showAbout: () => {}, }; const rpc = createAppRPC({ registry, engine, store, settings, edits, watcher, events }); @@ -108,29 +110,43 @@ const manager = new ManagerWindow({ rebuildMenu: () => void applyApplicationMenu(store), onSettingsSaved: (s) => settingsSaved(s, "manager"), }); +const settingsWin = new SettingsWindow({ + settings, + url, + currentSettings: () => currentSettings, + mainFrame: () => win.getFrame(), + onSettingsSaved: (s) => settingsSaved(s, "settings"), + showAbout: () => showAbout(), +}); events.openEditor = (ctx) => editors.open(ctx); /** * Every settings save lands here, whichever window made it (editor and manager - * windows save too — the scale shortcuts): side effects run once, then every - * OTHER window follows. The sender is skipped — it already shows the new - * values, and an echo arriving behind a newer local change (a held Cmd+) would - * drag it backwards. + * windows save too — the scale shortcuts — and the Settings window is nothing + * but saves): side effects run once, then every OTHER window follows. The + * sender is skipped — it already shows the new values, and an echo arriving + * behind a newer local change (a held Cmd+) would drag it backwards. */ -function settingsSaved(s: AppSettings, sender: "main" | "manager" | { editor: string }): void { +function settingsSaved( + s: AppSettings, + sender: "main" | "manager" | "settings" | { editor: string }, +): void { engine.setConcurrency(effectiveConcurrency(s)); const localeChanged = s.locale !== currentSettings.locale; currentSettings = s; // future windows' preloads bake the fresh theme if (localeChanged) { setBackendLocale(s.locale); void applyApplicationMenu(store); // relabel the native menu in the new language + settingsWin.retitle(); } if (sender !== "main") rpc.send.settingsChanged({ settings: s }); if (sender !== "manager") manager.broadcastSettings(s); + if (sender !== "settings") settingsWin.broadcastSettings(s); editors.broadcastSettings(s, typeof sender === "object" ? sender.editor : undefined); } events.settingsChanged = (s) => settingsSaved(s, "main"); events.openConnectionManager = () => manager.open(); +events.openSettings = () => settingsWin.open(); function showAbout(): void { const { build } = BRANDING; @@ -149,6 +165,7 @@ events.showAbout = showAbout; // and the modal manager has nothing to guard win.on("close", () => { manager.close(); + settingsWin.close(); editors.closeAllForAppExit(); }); @@ -173,6 +190,7 @@ engine.onUpdate = (snapshot) => { if ( !windowFocused && !editors.anyFocused() && + !settingsWin.isFocused() && (snapshot.state === "completed" || snapshot.state === "failed") ) { Utils.showNotification({ @@ -230,10 +248,14 @@ void applyApplicationMenu(store); ApplicationMenu.on("application-menu-clicked", (event) => { const action = (event as { data?: { action?: string } }).data?.action; if (!action) return; - // ⌘W while an editor window is focused closes that editor (with its dirty - // prompt), not a main-window tab. Other menu actions deliberately still - // target the main window. + // ⌘W while the Settings window or an editor window is focused closes THAT + // window (an editor with its dirty prompt), not a main-window tab. Other + // menu actions deliberately still target the main window. if (action === "close-tab") { + if (settingsWin.isFocused()) { + settingsWin.close(); + return; + } const focusedEditor = editors.focusedKey(); if (focusedEditor) { editors.requestClose(focusedEditor); @@ -244,6 +266,10 @@ ApplicationMenu.on("application-menu-clicked", (event) => { manager.open(); return; } + if (action === "settings") { + settingsWin.open(); + return; + } if (action === "about") { showAbout(); return; diff --git a/src/bun/menu.ts b/src/bun/menu.ts index edf6506..0cf7412 100644 --- a/src/bun/menu.ts +++ b/src/bun/menu.ts @@ -36,6 +36,8 @@ export async function applyApplicationMenu(store: ConnectionStore): Promise void; settingsChanged: (settings: AppSettings) => void; openConnectionManager: () => void; + openSettings: () => void; showAbout: () => void; }; } @@ -130,6 +131,28 @@ const pickKeyFileHandler = () => return { path }; }); +/** External-editor picker, shared by the main and settings window rpcs. */ +const pickEditorAppHandler = () => + wrap(async () => { + // PORTABILITY: apps live in different places per OS + const { homedir } = await import("node:os"); + const [startingFolder, allowedFileTypes] = + process.platform === "darwin" + ? ["/Applications", "app"] + : process.platform === "win32" + ? [process.env.ProgramFiles ?? "C:\\Program Files", "exe"] + : [homedir(), "*"]; + const picked = await Utils.openFileDialog({ + startingFolder, + allowedFileTypes, + canChooseFiles: true, + canChooseDirectory: false, + allowsMultipleSelection: false, + }); + const path = picked.find((p) => p.trim().length > 0) ?? null; + return { path }; + }); + export function createAppRPC({ registry, engine, @@ -257,26 +280,7 @@ export function createAppRPC({ editExternal: ({ sessionId, path }) => wrap(() => edits.editExternal(sessionId, path)), - pickEditorApp: () => - wrap(async () => { - // PORTABILITY: apps live in different places per OS - const { homedir } = await import("node:os"); - const [startingFolder, allowedFileTypes] = - process.platform === "darwin" - ? ["/Applications", "app"] - : process.platform === "win32" - ? [process.env.ProgramFiles ?? "C:\\Program Files", "exe"] - : [homedir(), "*"]; - const picked = await Utils.openFileDialog({ - startingFolder, - allowedFileTypes, - canChooseFiles: true, - canChooseDirectory: false, - allowsMultipleSelection: false, - }); - const path = picked.find((p) => p.trim().length > 0) ?? null; - return { path }; - }), + pickEditorApp: pickEditorAppHandler, pickKeyFile: pickKeyFileHandler, @@ -318,6 +322,13 @@ export function createAppRPC({ return {}; }), + // ---- settings window ---- + openSettings: () => + wrap(async () => { + events.openSettings(); + return {}; + }), + showAbout: () => wrap(async () => { events.showAbout(); @@ -449,3 +460,28 @@ export function createManagerRPC( }, }); } + +export interface SettingsRpcHooks { + /** Every edit here is a settings save — rebroadcast it to the other windows. */ + onSettingsSaved: (next: AppSettings) => void; + showAbout: () => void; +} + +/** RPC for the single Settings window (per-window instance). */ +export function createSettingsRPC(deps: { settings: SettingsStore }, hooks: SettingsRpcHooks) { + return BrowserView.defineRPC({ + maxRequestTime: MAX_REQUEST_TIME_MS, + handlers: { + requests: { + getSettings: () => wrap(async () => ({ settings: await deps.settings.load() })), + saveSettings: saveSettingsHandler(deps.settings, hooks.onSettingsSaved), + pickEditorApp: pickEditorAppHandler, + showAbout: () => + wrap(async () => { + hooks.showAbout(); + return {}; + }), + }, + }, + }); +} diff --git a/src/bun/settings-window.ts b/src/bun/settings-window.ts new file mode 100644 index 0000000..c609d1f --- /dev/null +++ b/src/bun/settings-window.ts @@ -0,0 +1,91 @@ +import type { BrowserWindow } from "electrobun/bun"; +import type { AppSettings } from "../shared/types"; +import { t } from "./i18n"; +import { createSettingsRPC } from "./rpc-handlers"; +import type { SettingsStore } from "./store/settings"; +import { openSecondaryWindow } from "./windows"; + +const SETTINGS_WIDTH = 680; +const SETTINGS_HEIGHT = 480; + +export interface SettingsWindowDeps { + settings: SettingsStore; + /** The once-resolved dev/prod view url. */ + url: string; + /** Live settings snapshot — baked into the window's preload (theme flash). */ + currentSettings: () => AppSettings; + mainFrame: () => { x: number; y: number; width: number; height: number }; + /** This window saved settings — rebroadcast them to every other window. */ + onSettingsSaved: (settings: AppSettings) => void; + showAbout: () => void; +} + +/** + * The single Settings window. Unlike the connection manager it is NOT modal: + * every edit is an ordinary settings save, so the other windows follow live + * while it stays open. On macOS ⌘W reaches bun as the main window's close-tab + * menu action, so index.ts asks isFocused() first and closes this window + * instead of a main-window tab. + */ +export class SettingsWindow { + private win: BrowserWindow | null = null; + private rpc: ReturnType | null = null; + private focused = false; + + constructor(private deps: SettingsWindowDeps) {} + + isFocused(): boolean { + return this.focused; + } + + open(): void { + if (this.win) { + this.win.activate(); + return; + } + + this.rpc = createSettingsRPC( + { settings: this.deps.settings }, + { + onSettingsSaved: (next) => this.deps.onSettingsSaved(next), + showAbout: () => this.deps.showAbout(), + }, + ); + + const main = this.deps.mainFrame(); + this.win = openSecondaryWindow({ + title: t("options.title"), + url: this.deps.url, + mode: "settings", + settings: this.deps.currentSettings(), + frame: { + x: Math.max(0, main.x + (main.width - SETTINGS_WIDTH) / 2), + y: Math.max(0, main.y + (main.height - SETTINGS_HEIGHT) / 2), + width: SETTINGS_WIDTH, + height: SETTINGS_HEIGHT, + }, + rpc: this.rpc, + }); + this.win.on("focus", () => (this.focused = true)); + this.win.on("blur", () => (this.focused = false)); + this.win.on("close", () => { + this.win = null; + this.rpc = null; + this.focused = false; + }); + console.log("[settings] window opened"); + } + + close(): void { + this.win?.close(); // the close event does the cleanup + } + + /** The native title follows a language switch (the header row is the UI's own). */ + retitle(): void { + this.win?.setTitle(t("options.title")); + } + + broadcastSettings(settings: AppSettings): void { + this.rpc?.send.settingsChanged({ settings }); + } +} diff --git a/src/bun/windows.ts b/src/bun/windows.ts index 445725c..a3e4f96 100644 --- a/src/bun/windows.ts +++ b/src/bun/windows.ts @@ -13,7 +13,7 @@ import type { AppSettings } from "../shared/types"; * windows flash the wrong background. */ export function buildPreload( - mode: "main" | "editor" | "manager", + mode: "main" | "editor" | "manager" | "settings", settings: AppSettings, ctx?: unknown, ): string { @@ -76,7 +76,7 @@ export interface SecondaryWindowOptions { title: string; /** The once-resolved dev/prod url (see getMainViewUrl in index.ts). */ url: string; - mode: "editor" | "manager"; + mode: "editor" | "manager" | "settings"; /** Current persisted settings — baked into the preload for flash-free paint. */ settings: AppSettings; ctx?: unknown; diff --git a/src/shared/i18n/de.ts b/src/shared/i18n/de.ts index 001265f..1173417 100644 --- a/src/shared/i18n/de.ts +++ b/src/shared/i18n/de.ts @@ -23,14 +23,22 @@ export const de = { manageConnections: "Verbindungen verwalten", newTab: "Neuer Tab ({mod}T)", closeTab: "Tab schließen ({mod}W)", - options: "Optionen", + options: "Einstellungen", update: "Update verfügbar", updating: "Aktualisiere", restartToUpdate: "Zum Anwenden neu starten", updateTooltip: "Update verfügbar: miniSCP {version}", }, options: { - title: "Optionen", + title: "Einstellungen", + sections: { + appearance: "Darstellung", + transfers: "Übertragungen", + editor: "Editor", + fileList: "Dateiliste", + about: "Über", + }, + columns: "Sichtbare Spalten", language: "Sprache", system: "System", theme: { label: "Design", system: "System", light: "Hell", dark: "Dunkel" }, @@ -248,6 +256,7 @@ export const de = { }, menu: { about: "Über {app}", + settings: "Einstellungen…", checkForUpdates: "Nach Updates suchen…", quit: "Beenden", file: "Datei", diff --git a/src/shared/i18n/en.ts b/src/shared/i18n/en.ts index 1256ae6..850e348 100644 --- a/src/shared/i18n/en.ts +++ b/src/shared/i18n/en.ts @@ -27,14 +27,22 @@ export const en = { manageConnections: "Manage connections", newTab: "New tab ({mod}T)", closeTab: "Close tab ({mod}W)", - options: "Options", + options: "Settings", update: "Update available", updating: "Updating", restartToUpdate: "Restart to apply", updateTooltip: "Update available: miniSCP {version}", }, options: { - title: "Options", + title: "Settings", + sections: { + appearance: "Appearance", + transfers: "Transfers", + editor: "Editor", + fileList: "File list", + about: "About", + }, + columns: "Visible columns", language: "Language", system: "System", theme: { label: "Theme", system: "System", light: "Light", dark: "Dark" }, @@ -252,6 +260,7 @@ export const en = { }, menu: { about: "About {app}", + settings: "Settings…", checkForUpdates: "Check for Updates…", quit: "Quit", file: "File", diff --git a/src/shared/i18n/es.ts b/src/shared/i18n/es.ts index 41d02a7..22f1a21 100644 --- a/src/shared/i18n/es.ts +++ b/src/shared/i18n/es.ts @@ -23,14 +23,22 @@ export const es = { manageConnections: "Gestionar conexiones", newTab: "Nueva pestaña ({mod}T)", closeTab: "Cerrar pestaña ({mod}W)", - options: "Opciones", + options: "Ajustes", update: "Actualización disponible", updating: "Actualizando", restartToUpdate: "Reiniciar para aplicar", updateTooltip: "Actualización disponible: miniSCP {version}", }, options: { - title: "Opciones", + title: "Ajustes", + sections: { + appearance: "Apariencia", + transfers: "Transferencias", + editor: "Editor", + fileList: "Lista de archivos", + about: "Acerca de", + }, + columns: "Columnas visibles", language: "Idioma", system: "Sistema", theme: { label: "Tema", system: "Sistema", light: "Claro", dark: "Oscuro" }, @@ -248,6 +256,7 @@ export const es = { }, menu: { about: "Acerca de {app}", + settings: "Ajustes…", checkForUpdates: "Buscar actualizaciones…", quit: "Salir", file: "Archivo", diff --git a/src/shared/i18n/eu.ts b/src/shared/i18n/eu.ts index 52df458..32f672d 100644 --- a/src/shared/i18n/eu.ts +++ b/src/shared/i18n/eu.ts @@ -25,14 +25,22 @@ export const eu = { manageConnections: "Kudeatu konexioak", newTab: "Fitxa berria ({mod}T)", closeTab: "Itxi fitxa ({mod}W)", - options: "Aukerak", + options: "Ezarpenak", update: "Eguneratzea eskuragarri", updating: "Eguneratzen", restartToUpdate: "Berrabiarazi aplikatzeko", updateTooltip: "Eguneratzea eskuragarri: miniSCP {version}", }, options: { - title: "Aukerak", + title: "Ezarpenak", + sections: { + appearance: "Itxura", + transfers: "Transferentziak", + editor: "Editorea", + fileList: "Fitxategi-zerrenda", + about: "Honi buruz", + }, + columns: "Zutabe ikusgaiak", language: "Hizkuntza", system: "Sistema", theme: { label: "Gaia", system: "Sistema", light: "Argia", dark: "Iluna" }, @@ -250,6 +258,7 @@ export const eu = { }, menu: { about: "{app} (r)i buruz", + settings: "Ezarpenak…", checkForUpdates: "Bilatu eguneratzeak…", quit: "Irten", file: "Fitxategia", diff --git a/src/shared/i18n/fr.ts b/src/shared/i18n/fr.ts index e8f6845..a2411fc 100644 --- a/src/shared/i18n/fr.ts +++ b/src/shared/i18n/fr.ts @@ -23,14 +23,22 @@ export const fr = { manageConnections: "Gérer les connexions", newTab: "Nouvel onglet ({mod}T)", closeTab: "Fermer l'onglet ({mod}W)", - options: "Options", + options: "Réglages", update: "Mise à jour disponible", updating: "Mise à jour", restartToUpdate: "Redémarrer pour appliquer", updateTooltip: "Mise à jour disponible : miniSCP {version}", }, options: { - title: "Options", + title: "Réglages", + sections: { + appearance: "Apparence", + transfers: "Transferts", + editor: "Éditeur", + fileList: "Liste des fichiers", + about: "À propos", + }, + columns: "Colonnes visibles", language: "Langue", system: "Système", theme: { label: "Thème", system: "Système", light: "Clair", dark: "Sombre" }, @@ -248,6 +256,7 @@ export const fr = { }, menu: { about: "À propos de {app}", + settings: "Réglages…", checkForUpdates: "Rechercher des mises à jour…", quit: "Quitter", file: "Fichier", diff --git a/src/shared/i18n/ja.ts b/src/shared/i18n/ja.ts index 76fedf4..7a00c57 100644 --- a/src/shared/i18n/ja.ts +++ b/src/shared/i18n/ja.ts @@ -24,14 +24,22 @@ export const ja = { manageConnections: "接続を管理", newTab: "新しいタブ ({mod}T)", closeTab: "タブを閉じる ({mod}W)", - options: "オプション", + options: "設定", update: "アップデートあり", updating: "更新中", restartToUpdate: "再起動して適用", updateTooltip: "アップデートがあります: miniSCP {version}", }, options: { - title: "オプション", + title: "設定", + sections: { + appearance: "外観", + transfers: "転送", + editor: "エディタ", + fileList: "ファイル一覧", + about: "情報", + }, + columns: "表示する列", language: "言語", system: "システム", theme: { label: "テーマ", system: "システム", light: "ライト", dark: "ダーク" }, @@ -248,6 +256,7 @@ export const ja = { }, menu: { about: "{app} について", + settings: "設定…", checkForUpdates: "アップデートを確認…", quit: "終了", file: "ファイル", diff --git a/src/shared/i18n/zh.ts b/src/shared/i18n/zh.ts index a6dd65b..af8a01b 100644 --- a/src/shared/i18n/zh.ts +++ b/src/shared/i18n/zh.ts @@ -24,14 +24,22 @@ export const zh = { manageConnections: "管理连接", newTab: "新建标签页 ({mod}T)", closeTab: "关闭标签页 ({mod}W)", - options: "选项", + options: "设置", update: "有可用更新", updating: "正在更新", restartToUpdate: "重启以应用", updateTooltip: "有可用更新:miniSCP {version}", }, options: { - title: "选项", + title: "设置", + sections: { + appearance: "外观", + transfers: "传输", + editor: "编辑器", + fileList: "文件列表", + about: "关于", + }, + columns: "显示的列", language: "语言", system: "系统", theme: { label: "主题", system: "系统", light: "浅色", dark: "深色" }, @@ -248,6 +256,7 @@ export const zh = { }, menu: { about: "关于 {app}", + settings: "设置…", checkForUpdates: "检查更新…", quit: "退出", file: "文件", diff --git a/src/shared/rpc.ts b/src/shared/rpc.ts index c139bbd..255fd47 100644 --- a/src/shared/rpc.ts +++ b/src/shared/rpc.ts @@ -26,9 +26,9 @@ export interface EditorOpenContext { // bun.requests = calls the webview makes, handled in src/bun/rpc-handlers.ts // webview.messages = push events from bun, handled in ui rpc.service.ts // -// ONE schema serves both window kinds (main + editor): electrobun allows -// partial handler sets, and messages nobody listens to are silently dropped, -// so each side only implements/hears what applies to it. +// ONE schema serves every window kind (main, editor, manager, settings): +// electrobun allows partial handler sets, and messages nobody listens to are +// silently dropped, so each side only implements/hears what applies to it. export type AppRPC = { bun: { requests: { @@ -154,6 +154,9 @@ export type AppRPC = { managerConnect: { params: { id: string }; response: Result }; managerOpenStack: { params: { id: string }; response: Result }; managerClose: { params: Record; response: Result }; + // settings window + /** Main window: open (or focus) the single Settings window. */ + openSettings: { params: Record; response: Result }; // auto-updater (electrobun Updater) /** Main window: re-run the update check on demand (menu item). */ checkForUpdate: { diff --git a/ui/src/app/core/context-menu.service.ts b/ui/src/app/core/context-menu.service.ts index dce9365..d0d5836 100644 --- a/ui/src/app/core/context-menu.service.ts +++ b/ui/src/app/core/context-menu.service.ts @@ -5,7 +5,7 @@ import { IS_MAC, IS_WINDOWS, MOD_LABEL } from './platform'; import { RpcService } from './rpc.service'; /** Optional list columns, in display order (Name is fixed). */ -const COLUMN_KEYS: ColumnKey[] = ['size', 'mtime', 'kind', 'rights', 'owner', 'group']; +export const COLUMN_KEYS: ColumnKey[] = ['size', 'mtime', 'kind', 'rights', 'owner', 'group']; export interface MenuItem { type?: 'separator'; diff --git a/ui/src/app/core/icons.ts b/ui/src/app/core/icons.ts index bbd0f1a..b658cd9 100644 --- a/ui/src/app/core/icons.ts +++ b/ui/src/app/core/icons.ts @@ -18,7 +18,6 @@ import { tdesignDelete, tdesignDownload, tdesignEdit, - tdesignEllipsis, tdesignFile, tdesignFile1, tdesignFileAdd, @@ -54,7 +53,6 @@ export const APP_ICONS = { tdesignDelete, tdesignDownload, tdesignEdit, - tdesignEllipsis, tdesignFile, tdesignFile1, tdesignFileAdd, diff --git a/ui/src/app/core/platform.ts b/ui/src/app/core/platform.ts index 8cecbb1..203ce33 100644 --- a/ui/src/app/core/platform.ts +++ b/ui/src/app/core/platform.ts @@ -7,7 +7,7 @@ declare global { interface Window { __APP_PLATFORM?: string; /** Which root component this window boots. */ - __APP_MODE?: 'main' | 'editor' | 'manager'; + __APP_MODE?: 'main' | 'editor' | 'manager' | 'settings'; /** Window-kind specific context (EditorOpenContext for editor windows). */ __APP_CTX?: unknown; } @@ -25,7 +25,7 @@ export const IS_LINUX = PLATFORM === 'linux'; export const MOD_LABEL = IS_MAC ? '⌘' : 'Ctrl+'; export const ALT_LABEL = IS_MAC ? '⌥' : 'Alt+'; -export const APP_MODE: 'main' | 'editor' | 'manager' = +export const APP_MODE: 'main' | 'editor' | 'manager' | 'settings' = typeof window !== 'undefined' ? (window.__APP_MODE ?? 'main') : 'main'; export const EDITOR_CONTEXT = diff --git a/ui/src/app/core/shortcuts.service.ts b/ui/src/app/core/shortcuts.service.ts index 56e95e6..513fa15 100644 --- a/ui/src/app/core/shortcuts.service.ts +++ b/ui/src/app/core/shortcuts.service.ts @@ -13,6 +13,7 @@ export type ShortcutId = | 'cycle-tab' | 'cycle-tab-back' | 'go-to-tab' // 1-9; the digit rides in ShortcutMatch.digit + | 'open-settings' // editor window | 'save' | 'find' @@ -29,11 +30,11 @@ export interface ShortcutMatch { digit?: number; } -/** Which keymap a window consults. 'manager' answers only to the any-window - * shortcuts (scale). Escape/Tab stay component-local: their meaning is - * contextual state (find bar open? close prompt up? which pane?), not a +/** Which keymap a window consults. 'manager' and 'settings' answer only to the + * any-window shortcuts (scale). Escape/Tab stay component-local: their meaning + * is contextual state (find bar open? close prompt up? which pane?), not a * global binding. */ -export type ShortcutScope = 'main' | 'editor' | 'manager'; +export type ShortcutScope = 'main' | 'editor' | 'manager' | 'settings'; /** * The keyboard layer: modifier helpers + one declarative keymap. @@ -83,9 +84,11 @@ export class ShortcutsService { if (event.code === 'Digit0' || event.code === 'Numpad0') return { id: 'scale-reset' }; } - if (scope === 'manager') return null; // any-window shortcuts only + if (scope === 'manager' || scope === 'settings') return null; // any-window shortcuts only if (scope === 'main') { + // on macOS the app menu's ⌘, intercepts first; this is the Windows/Linux path + if (mod && key === ',') return { id: 'open-settings' }; if (mod && key === 't') return { id: 'new-tab' }; if (mod && key === 'w') return { id: 'close-tab' }; // ctrlKey only, never meta: ⌘Tab is the macOS app switcher diff --git a/ui/src/app/skins/shadlike/app.ts b/ui/src/app/skins/shadlike/app.ts index ac81bfa..a87b730 100644 --- a/ui/src/app/skins/shadlike/app.ts +++ b/ui/src/app/skins/shadlike/app.ts @@ -188,6 +188,9 @@ export class App { // ⌘1–⌘8 jump to that tab, ⌘9 to the last, like Firefox this.panels.goToWorkspace(match.digit === 9 ? -1 : match.digit! - 1); break; + case 'open-settings': + void this.rpc.request.openSettings({}); + break; case 'scale-up': this.settings.setUiScale(this.settings.uiScale() + 0.1); break; diff --git a/ui/src/app/skins/shadlike/index.ts b/ui/src/app/skins/shadlike/index.ts index 2c316fb..aa300fc 100644 --- a/ui/src/app/skins/shadlike/index.ts +++ b/ui/src/app/skins/shadlike/index.ts @@ -1,9 +1,10 @@ import { App } from './app'; import { ConnectionManager } from './connection-manager/connection-manager'; import { EditorWindow } from './editor-window/editor-window'; +import { SettingsWindow } from './settings-window/settings-window'; /** - * The three window roots, keyed by APP_MODE — main.ts picks one. There is no + * The four window roots, keyed by APP_MODE — main.ts picks one. There is no * title-bar component: the native window frame is kept (hiddenInset on macOS, * which is what IS_MAC_CHROME accounts for). */ @@ -11,4 +12,5 @@ export const SHAD_ROOTS = { main: App, manager: ConnectionManager, editor: EditorWindow, + settings: SettingsWindow, } as const; diff --git a/ui/src/app/skins/shadlike/settings-window/settings-window.html b/ui/src/app/skins/shadlike/settings-window/settings-window.html new file mode 100644 index 0000000..0fa6078 --- /dev/null +++ b/ui/src/app/skins/shadlike/settings-window/settings-window.html @@ -0,0 +1,303 @@ +
+ +
+ +

{{ i18n.t('options.title') }}

+
+ +
+ + + + +
+

{{ i18n.t('options.sections.' + section()) }}

+
+ @switch (section()) { + @case ('appearance') { +
+ {{ i18n.t('options.language') }} + +
+ +
+ {{ i18n.t('options.theme.label') }} +
+ @for (theme of themes; track theme) { + + } +
+
+ + @if (settings.effectiveDark()) { +
+ {{ i18n.t('options.tone.label') }} +
+ @for (tone of darkTones; track tone) { + + } +
+
+ } + +
+ {{ i18n.t('options.accent.label') }} +
+ @for (tone of accents; track tone.value) { + + } +
+
+ + +
+ {{ i18n.t('options.interface.label') }} +
+ + + +
+
+ } + + @case ('transfers') { +
+ {{ i18n.t('options.speedUnit.label') }} +
+ @for (unit of speedUnits; track unit.value) { + + } +
+
+ +
+ {{ i18n.t('options.transfers.label') }} +
+ @for (mode of transferModes; track mode) { + + } +
+
+ + @if (settings.transferMode() === 'multi') { +
+ {{ i18n.t('options.atOnce') }} +
+ @for (count of concurrencyChoices; track count) { + + } +
+
+ } + +
+ {{ i18n.t('options.finishedTransfers.label') }} +
+ + +
+
+ } + + @case ('editor') { +
+ {{ i18n.t('options.editor.label') }} +
+ + +
+
+ + @if (settings.editor() === 'custom') { +
+ + {{ editorAppName() }} + +
+ + @if (settings.editorApp()) { + + } +
+
+ } + } + + @case ('fileList') { + {{ i18n.t('options.columns') }} +
+ @for (key of columnKeys; track key) { + + } +
+ } + + @case ('about') { + + + } + } +
+
+
+
diff --git a/ui/src/app/skins/shadlike/settings-window/settings-window.ts b/ui/src/app/skins/shadlike/settings-window/settings-window.ts new file mode 100644 index 0000000..a8c183f --- /dev/null +++ b/ui/src/app/skins/shadlike/settings-window/settings-window.ts @@ -0,0 +1,105 @@ +import { Component, computed, inject, signal } from '@angular/core'; +import { NgIcon } from '@ng-icons/core'; +import { BRANDING } from '@shared/branding'; +import { LOCALE_LABELS, SUPPORTED } from '@shared/i18n'; +import type { AccentTone } from '@shared/types'; +import { COLUMN_KEYS } from '@core/context-menu.service'; +import { I18nService } from '@core/i18n.service'; +import { blockNativeContextMenu, IS_MAC_CHROME } from '@core/platform'; +import { RpcService } from '@core/rpc.service'; +import { SettingsService } from '@core/settings.service'; +import { ShortcutsService } from '@core/shortcuts.service'; + +const SECTIONS = ['appearance', 'transfers', 'editor', 'fileList', 'about'] as const; +type Section = (typeof SECTIONS)[number]; + +const THEMES = ['system', 'light', 'dark'] as const; +const DARK_TONES = ['dark', 'darker'] as const; +// Swatch colors are fixed mid-tones so every dot reads on both light and dark +// pages (the live --primary values would vanish against one of them). Names are +// translated via options.accentNames.. +const ACCENTS: readonly { value: AccentTone; swatch: string }[] = [ + { value: 'graphite', swatch: 'oklch(0.552 0.016 285.9)' }, + { value: 'blue', swatch: 'oklch(0.65 0.17 255)' }, + { value: 'indigo', swatch: 'oklch(0.65 0.19 277)' }, + { value: 'purple', swatch: 'oklch(0.65 0.17 305)' }, + { value: 'rose', swatch: 'oklch(0.65 0.17 8)' }, + { value: 'orange', swatch: 'oklch(0.68 0.17 50)' }, + { value: 'green', swatch: 'oklch(0.65 0.15 150)' }, + { value: 'teal', swatch: 'oklch(0.65 0.12 185)' }, +]; +const SPEED_UNITS = [ + { value: 'MBps', label: 'MB/s' }, + { value: 'Mbps', label: 'Mbps' }, +] as const; +const TRANSFER_MODES = ['single', 'multi'] as const; +const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const; + +/** + * Root of the Settings window (APP_MODE 'settings'). Every control is a plain + * SettingsService setter: the save goes to bun, which rebroadcasts it, so the + * other windows follow live while this one stays open. + */ +@Component({ + selector: 'app-root', + imports: [NgIcon], + templateUrl: './settings-window.html', + host: { + '(window:keydown)': 'onWindowKeydown($event)', + '(window:contextmenu)': 'blockContextMenu($event)', + }, +}) +export class SettingsWindow { + readonly settings = inject(SettingsService); + readonly i18n = inject(I18nService); + private readonly rpc = inject(RpcService); + private readonly shortcuts = inject(ShortcutsService); + + readonly isMacChrome = IS_MAC_CHROME; + readonly sections = SECTIONS; + readonly section = signal
('appearance'); + readonly localeOptions = SUPPORTED; + readonly localeLabels = LOCALE_LABELS; + readonly themes = THEMES; + readonly darkTones = DARK_TONES; + readonly accents = ACCENTS; + readonly speedUnits = SPEED_UNITS; + readonly transferModes = TRANSFER_MODES; + readonly concurrencyChoices = CONCURRENCY_CHOICES; + readonly columnKeys = COLUMN_KEYS; + readonly appName = BRANDING.appName; + readonly buildLabel = BRANDING.buildLabel; + /** "100%" for the stepper readout; the value itself lives in settings. */ + readonly scalePercent = computed(() => `${Math.round(this.settings.uiScale() * 100)}%`); + + /** "Visual Studio Code.app" -> "Visual Studio Code"; empty -> system default. */ + editorAppName(): string { + const path = this.settings.editorApp(); + if (!path) return this.i18n.t('options.systemDefault'); + const base = path.split('/').filter(Boolean).pop() ?? path; + return base.replace(/\.app$/i, ''); + } + + async pickEditorApp(): Promise { + const res = await this.rpc.request.pickEditorApp({}); + if (res.ok && res.path) this.settings.setEditorApp(res.path); + } + + showAbout(): void { + void this.rpc.request.showAbout({}); + } + + /** Any-window shortcuts (interface scale); ⌘W arrives as a menu action bun-side. */ + onWindowKeydown(event: KeyboardEvent): void { + const match = this.shortcuts.resolve(event, 'settings'); + if (!match || this.shortcuts.isTypingTarget(event)) return; + event.preventDefault(); + if (match.id === 'scale-up') this.settings.setUiScale(this.settings.uiScale() + 0.1); + else if (match.id === 'scale-down') this.settings.setUiScale(this.settings.uiScale() - 0.1); + else if (match.id === 'scale-reset') this.settings.setUiScale(1); + } + + blockContextMenu(event: Event): void { + blockNativeContextMenu(event); + } +} diff --git a/ui/src/app/skins/shadlike/tab-bar/tab-bar.ts b/ui/src/app/skins/shadlike/tab-bar/tab-bar.ts index ee65277..4eacace 100644 --- a/ui/src/app/skins/shadlike/tab-bar/tab-bar.ts +++ b/ui/src/app/skins/shadlike/tab-bar/tab-bar.ts @@ -1,38 +1,12 @@ -import { Component, ElementRef, computed, effect, inject, signal, viewChild } from '@angular/core'; +import { Component, ElementRef, effect, inject, signal, viewChild } from '@angular/core'; import { NgIcon } from '@ng-icons/core'; -import { BRANDING } from '@shared/branding'; -import { LOCALE_LABELS, SUPPORTED } from '@shared/i18n'; -import type { AccentTone } from '@shared/types'; import { ContextMenuService } from '@core/context-menu.service'; import { I18nService } from '@core/i18n.service'; import { PanelsService, type Workspace } from '@core/panels.service'; import { IS_MAC_CHROME, MOD_LABEL } from '@core/platform'; import { RpcService } from '@core/rpc.service'; -import { SettingsService } from '@core/settings.service'; import { UpdaterService } from '@core/updater.service'; -const THEMES = ['system', 'light', 'dark'] as const; -const DARK_TONES = ['dark', 'darker'] as const; -// Swatch colors are fixed mid-tones so every dot reads on both light and dark -// cards (the live --primary values would vanish against one of them). Names are -// translated via options.accentNames.. -const ACCENTS: readonly { value: AccentTone; swatch: string }[] = [ - { value: 'graphite', swatch: 'oklch(0.552 0.016 285.9)' }, - { value: 'blue', swatch: 'oklch(0.65 0.17 255)' }, - { value: 'indigo', swatch: 'oklch(0.65 0.19 277)' }, - { value: 'purple', swatch: 'oklch(0.65 0.17 305)' }, - { value: 'rose', swatch: 'oklch(0.65 0.17 8)' }, - { value: 'orange', swatch: 'oklch(0.68 0.17 50)' }, - { value: 'green', swatch: 'oklch(0.65 0.15 150)' }, - { value: 'teal', swatch: 'oklch(0.65 0.12 185)' }, -]; -const SPEED_UNITS = [ - { value: 'MBps', label: 'MB/s' }, - { value: 'Mbps', label: 'Mbps' }, -] as const; -const TRANSFER_MODES = ['single', 'multi'] as const; -const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const; - @Component({ selector: 'app-tab-bar', imports: [NgIcon], @@ -154,302 +128,25 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const; {{ i18n.t('tabBar.connections') }} - + - - @if (settingsOpen()) { -
-
-

{{ i18n.t('options.title') }}

- -
- {{ i18n.t('options.language') }} - -
- -
- {{ i18n.t('options.theme.label') }} -
- @for (theme of themes; track theme) { - - } -
-
- - @if (settings.effectiveDark()) { -
- {{ i18n.t('options.tone.label') }} -
- @for (tone of darkTones; track tone) { - - } -
-
- } - -
- {{ i18n.t('options.accent.label') }} -
- @for (tone of accents; track tone.value) { - - } -
-
- - -
- {{ i18n.t('options.interface.label') }} -
- - - -
-
- -
- {{ i18n.t('options.speedUnit.label') }} -
- @for (unit of speedUnits; track unit.value) { - - } -
-
- -
- {{ i18n.t('options.transfers.label') }} -
- @for (mode of transferModes; track mode) { - - } -
-
- - @if (settings.transferMode() === 'multi') { -
- {{ i18n.t('options.atOnce') }} -
- @for (count of concurrencyChoices; track count) { - - } -
-
- } - -
- {{ i18n.t('options.editor.label') }} -
- - -
-
- - @if (settings.editor() === 'custom') { -
- - {{ editorAppName() }} - -
- - @if (settings.editorApp()) { - - } -
-
- } - -
- {{ i18n.t('options.finishedTransfers.label') }} -
- - -
-
- - - - - -
- } `, }) export class TabBar { readonly panels = inject(PanelsService); - readonly settings = inject(SettingsService); readonly i18n = inject(I18nService); readonly updater = inject(UpdaterService); private readonly contextMenu = inject(ContextMenuService); private readonly rpc = inject(RpcService); - readonly settingsOpen = signal(false); - readonly localeOptions = SUPPORTED; - readonly localeLabels = LOCALE_LABELS; readonly isMacChrome = IS_MAC_CHROME; readonly modLabel = MOD_LABEL; - readonly appName = BRANDING.appName; - readonly buildLabel = BRANDING.buildLabel; - readonly themes = THEMES; - readonly darkTones = DARK_TONES; - readonly accents = ACCENTS; - readonly speedUnits = SPEED_UNITS; - /** "100%" for the stepper readout; the value itself lives in settings. */ - readonly scalePercent = computed(() => `${Math.round(this.settings.uiScale() * 100)}%`); - readonly transferModes = TRANSFER_MODES; - readonly concurrencyChoices = CONCURRENCY_CHOICES; // drag & drop reordering readonly draggedId = signal(null); @@ -506,18 +203,14 @@ export class TabBar { ); } - /** "Visual Studio Code.app" -> "Visual Studio Code"; empty -> system default. */ - editorAppName(): string { - const path = this.settings.editorApp(); - if (!path) return this.i18n.t('options.systemDefault'); - const base = path.split('/').filter(Boolean).pop() ?? path; - return base.replace(/\.app$/i, ''); - } - openManager(): void { void this.rpc.request.openConnectionManager({}); } + openSettings(): void { + void this.rpc.request.openSettings({}); + } + /** Update pill: first click downloads, second click (when ready) applies + relaunches. */ onUpdateClick(): void { const state = this.updater.state(); @@ -525,15 +218,6 @@ export class TabBar { else if (state === 'ready') this.updater.apply(); } - showAbout(): void { - void this.rpc.request.showAbout({}); - } - - async pickEditorApp(): Promise { - const res = await this.rpc.request.pickEditorApp({}); - if (res.ok && res.path) this.settings.setEditorApp(res.path); - } - /** Insertion line at the drop position (inset shadow: no layout shift). */ dropIndicatorShadow(index: number): string | null { const target = this.dropTarget(); diff --git a/ui/src/main.ts b/ui/src/main.ts index 359f75c..41ab2ad 100644 --- a/ui/src/main.ts +++ b/ui/src/main.ts @@ -3,7 +3,5 @@ import { appConfig } from '@core/app.config'; import { APP_MODE } from '@core/platform'; import { SHAD_ROOTS } from './app/skins/shadlike'; -// One bundle, three window kinds: the preload global picks the root component. -const root = - APP_MODE === 'editor' ? SHAD_ROOTS.editor : APP_MODE === 'manager' ? SHAD_ROOTS.manager : SHAD_ROOTS.main; -bootstrapApplication(root, appConfig).catch((err) => console.error(err)); +// One bundle, four window kinds: the preload global picks the root component. +bootstrapApplication(SHAD_ROOTS[APP_MODE], appConfig).catch((err) => console.error(err));