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
+79 -4
View File
@@ -1,15 +1,31 @@
import { Component, inject } from '@angular/core';
import { Component, inject, signal } from '@angular/core';
import { NgIcon } from '@ng-icons/core';
import { PanelsService } from '../panels.service';
import { IS_MAC_CHROME } from '../platform';
import { SettingsService } from '../settings.service';
const THEMES = ['system', 'light', 'dark'] as const;
const SPEED_UNITS = [
{ value: 'MBps', label: 'MB/s' },
{ value: 'Mbps', label: 'Mbps' },
] as const;
@Component({
selector: 'app-tab-bar',
imports: [NgIcon],
template: `
<div class="flex items-center gap-0.5 border-b border-border px-1.5 pt-1">
<!-- On macOS this row IS the titlebar (hiddenInset): taller, padded past the
traffic lights, and draggable. Elsewhere it's a normal compact row. -->
<div
class="relative flex items-center gap-0.5 border-b border-border pr-1.5 electrobun-webkit-app-region-drag"
[class.h-10]="isMacChrome"
[class.pl-20]="isMacChrome"
[class.pl-1.5]="!isMacChrome"
[class.pt-1]="!isMacChrome"
>
@for (ws of panels.workspaces(); track ws.id) {
<div
class="group relative flex max-w-52 items-center rounded-t-md"
class="group relative flex max-w-52 items-center rounded-md electrobun-webkit-app-region-no-drag"
[class.bg-accent]="ws.id === panels.activeWorkspaceId()"
>
<button
@@ -46,15 +62,74 @@ import { PanelsService } from '../panels.service';
<button
(click)="panels.newWorkspace()"
title="New tab (⌘T)"
class="ml-0.5 grid size-6 place-items-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground"
class="ml-0.5 grid size-6 place-items-center rounded-md text-muted-foreground electrobun-webkit-app-region-no-drag hover:bg-accent hover:text-foreground"
>
<ng-icon name="mynaPlus" size="0.9rem" />
</button>
<div class="flex-1"></div>
<!-- global options -->
<button
(click)="settingsOpen.set(!settingsOpen())"
title="Options"
class="grid size-6 place-items-center rounded-md text-muted-foreground electrobun-webkit-app-region-no-drag hover:bg-accent hover:text-foreground"
>
<ng-icon name="mynaCog" size="0.95rem" />
</button>
@if (settingsOpen()) {
<div class="fixed inset-0 z-20" (click)="settingsOpen.set(false)"></div>
<div
class="absolute top-full right-1.5 z-30 mt-1 w-64 rounded-lg border border-border bg-card p-3 text-card-foreground shadow-xl electrobun-webkit-app-region-no-drag"
>
<h3 class="text-xs font-semibold">Options</h3>
<div class="mt-3 flex items-center justify-between gap-2">
<span class="text-xs text-muted-foreground">Theme</span>
<div class="flex rounded-md border border-border p-0.5">
@for (theme of themes; track theme) {
<button
(click)="settings.setTheme(theme)"
class="rounded px-2 py-0.5 text-[11px] capitalize"
[class.bg-accent]="settings.theme() === theme"
[class.text-foreground]="settings.theme() === theme"
[class.text-muted-foreground]="settings.theme() !== theme"
>
{{ theme }}
</button>
}
</div>
</div>
<div class="mt-2.5 flex items-center justify-between gap-2">
<span class="text-xs text-muted-foreground">Speed unit</span>
<div class="flex rounded-md border border-border p-0.5">
@for (unit of speedUnits; track unit.value) {
<button
(click)="settings.setSpeedUnit(unit.value)"
class="rounded px-2 py-0.5 text-[11px]"
[class.bg-accent]="settings.speedUnit() === unit.value"
[class.text-foreground]="settings.speedUnit() === unit.value"
[class.text-muted-foreground]="settings.speedUnit() !== unit.value"
>
{{ unit.label }}
</button>
}
</div>
</div>
</div>
}
</div>
`,
})
export class TabBar {
readonly panels = inject(PanelsService);
readonly settings = inject(SettingsService);
readonly settingsOpen = signal(false);
readonly isMacChrome = IS_MAC_CHROME;
readonly themes = THEMES;
readonly speedUnits = SPEED_UNITS;
close(event: MouseEvent, id: number): void {
event.stopPropagation();