v1.2: overwrite confirm, transfer speed/ETA, settings + themes, fused macOS titlebar

- Overwrite confirmation: name-collision detection before enqueue; dialog shows
  source vs destination (size, mtime, 'newer' badge) with per-item Skip/Replace
  and batch Skip all / Replace all; type mismatches auto-skip with a toast;
  cancel aborts the whole operation
- Transfer speed + ETA: UI computes smoothed bytes/sec from progress deltas
  (callback bus makes deltas meaningful); shown in the expanded queue rows and
  the status-bar summary; unit follows the speed-unit setting
- Settings: bun-side SettingsStore (settings.json, 0600, atomic) + get/save RPC;
  SettingsService; gear popover on the tab row (theme segmented control, speed
  unit); persisted across restarts
- Light theme: data-theme on <html> drives token overrides; absent = follow OS
- macOS fused titlebar: hiddenInset + trafficLightOffset, tab row becomes the
  draggable titlebar (app-region drag/no-drag classes); other platforms keep a
  separate titlebar (gated by a platform value injected via window preload)
- README: known-issues note for the upstream Bun/Electrobun FFI crash

Verified: tsc + ui build clean; SettingsStore roundtrip + 0600 headless;
app boots and the UI fetches settings over RPC at startup.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jokin
2026-07-11 17:14:50 +02:00
co-authored by Claude Fable 5
parent 262f4de04d
commit 6048d5c814
19 changed files with 571 additions and 14 deletions
+25
View File
@@ -0,0 +1,25 @@
import fs from "node:fs/promises";
import path from "node:path";
import { DEFAULT_SETTINGS, type AppSettings } from "../../shared/types";
import { appDataDir } from "./paths";
/** App settings persisted bun-side (WKWebView localStorage on views:// is unreliable). */
export class SettingsStore {
private readonly file = path.join(appDataDir(), "settings.json");
async load(): Promise<AppSettings> {
try {
const data = JSON.parse(await fs.readFile(this.file, "utf8"));
return { ...DEFAULT_SETTINGS, ...data };
} catch {
return { ...DEFAULT_SETTINGS };
}
}
async save(settings: AppSettings): Promise<void> {
await fs.mkdir(path.dirname(this.file), { recursive: true });
const tmp = `${this.file}.tmp`;
await fs.writeFile(tmp, JSON.stringify(settings, null, "\t"), { mode: 0o600 });
await fs.rename(tmp, this.file);
}
}