Stage 4: restore the modern tree into app/skins/shadlike/
Both UIs now live side by side. The 18 modern component files come straight
from `git show main:<path>` — recovered, not rewritten, which is possible
because main's tip IS the merge base of this branch. There is no title-bar
among them: that component is win7-only, since the modern skin defers to the
native window frame.
Their imports were rewritten to @core the same way stage 1 did, leaving only
skin-internal relative imports behind. Added skins/shadlike/index.ts exporting
SHAD_ROOTS with the same shape as SEVEN_ROOTS, so main.ts can select a tree
without knowing either skin's internals.
Restored the skin axis in core/platform.ts, deleted in fe028a9 and needed
again by the modern tree: __APP_SKIN on the Window global, APP_SKIN,
IS_WIN7_CHROME, and IS_MAC_CHROME = IS_MAC && !IS_WIN7_CHROME. That last one
is what reserves pl-24 for the macOS traffic lights in the fused hiddenInset
header — and correctly stops reserving it under win7, where titleBarStyle
'hidden' removes the traffic lights entirely.
The tree is not reachable yet; the build is the gate. I checked that the gate
is real rather than assuming it: injecting a type error into an unreferenced
shadlike file does fail `bun run build:ui`, so ngtsc type-checks the whole
program including components nothing imports. Without that check, "the build
passes" would have proven nothing about 18 files no one references.
win7 remains pixel-identical to the pre-refactor baseline.
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
@if (transfers.expanded() && transfers.hasJobs()) {
|
||||
<div class="flex items-center justify-between border-t border-border bg-chrome px-3 py-1 text-[11px] text-muted-foreground">
|
||||
<span>
|
||||
{{ i18n.t('transfer.summary', { active: transfers.activeJobs().length, finished: transfers.finishedJobs().length }) }}
|
||||
</span>
|
||||
<button
|
||||
(click)="transfers.clearFinished()"
|
||||
[disabled]="!transfers.finishedJobs().length"
|
||||
class="flex items-center gap-1 rounded px-1.5 py-0.5 hover:bg-accent hover:text-foreground disabled:opacity-40 disabled:hover:bg-transparent"
|
||||
>
|
||||
<ng-icon name="mynaTrash" size="0.75rem" /> {{ i18n.t('transfer.clearFinished') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="max-h-40 overflow-y-auto border-t border-border bg-chrome">
|
||||
@for (job of transfers.jobs(); track job.id) {
|
||||
<div class="flex items-center gap-2 px-3 py-1.5 text-xs">
|
||||
<ng-icon [name]="stateIcon(job)" size="0.95rem" [class]="stateClass(job)" />
|
||||
<span class="w-40 shrink-0 truncate font-medium" [title]="job.srcPath">{{ job.name }}</span>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="truncate text-[11px] text-muted-foreground">{{ progressLabel(job) }}</div>
|
||||
@if (isActive(job)) {
|
||||
<div class="mt-0.5 h-1 overflow-hidden rounded-full bg-muted">
|
||||
<div
|
||||
class="h-full rounded-full bg-primary transition-[width] duration-100"
|
||||
[style.width.%]="jobPercent(job)"
|
||||
></div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@if (isActive(job)) {
|
||||
<button
|
||||
(click)="transfers.cancel(job.id)"
|
||||
[title]="i18n.t('transfer.cancel')"
|
||||
class="shrink-0 rounded p-1 text-muted-foreground hover:bg-accent hover:text-destructive"
|
||||
>
|
||||
<ng-icon name="mynaX" size="0.9rem" />
|
||||
</button>
|
||||
} @else {
|
||||
<button
|
||||
(click)="transfers.dismiss(job.id)"
|
||||
[title]="i18n.t('transfer.dismiss')"
|
||||
class="shrink-0 rounded p-1 text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
>
|
||||
<ng-icon name="mynaX" size="0.9rem" />
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { NgIcon } from '@ng-icons/core';
|
||||
import type { TransferSnapshot } from '@shared/types';
|
||||
import { formatEta } from '@core/format';
|
||||
import { I18nService } from '@core/i18n.service';
|
||||
import { SettingsService } from '@core/settings.service';
|
||||
import { TransfersService } from '@core/transfers.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-transfer-bar',
|
||||
imports: [NgIcon],
|
||||
templateUrl: './transfer-bar.html',
|
||||
})
|
||||
export class TransferBar {
|
||||
readonly i18n = inject(I18nService);
|
||||
readonly transfers = inject(TransfersService);
|
||||
readonly settings = inject(SettingsService);
|
||||
readonly formatSize = (bytes: number): string => this.i18n.size(bytes);
|
||||
|
||||
/** "14.2 MB/s · 0:42 left" while a job is moving bytes. */
|
||||
speedLabel(job: TransferSnapshot): string {
|
||||
if (job.state !== 'running') return '';
|
||||
const speed = this.transfers.speeds().get(job.id);
|
||||
if (!speed) return '';
|
||||
const parts = [this.i18n.speed(speed, this.settings.speedUnit())];
|
||||
const remaining = job.totalBytes - job.doneBytes;
|
||||
if (remaining > 0 && speed > 0) {
|
||||
parts.push(this.i18n.t('transfer.left', { eta: formatEta(remaining / speed) }));
|
||||
}
|
||||
return parts.join(' · ');
|
||||
}
|
||||
|
||||
stateIcon(job: TransferSnapshot): string {
|
||||
switch (job.state) {
|
||||
case 'completed':
|
||||
return 'mynaCheckCircle';
|
||||
case 'failed':
|
||||
case 'cancelled':
|
||||
return 'mynaXCircle';
|
||||
default:
|
||||
return 'mynaSpinner';
|
||||
}
|
||||
}
|
||||
|
||||
stateClass(job: TransferSnapshot): string {
|
||||
switch (job.state) {
|
||||
case 'completed':
|
||||
return 'shrink-0 text-success';
|
||||
case 'failed':
|
||||
return 'shrink-0 text-destructive';
|
||||
case 'cancelled':
|
||||
return 'shrink-0 text-muted-foreground';
|
||||
default:
|
||||
return 'shrink-0 animate-spin text-primary';
|
||||
}
|
||||
}
|
||||
|
||||
isActive(job: TransferSnapshot): boolean {
|
||||
return job.state === 'queued' || job.state === 'scanning' || job.state === 'running';
|
||||
}
|
||||
|
||||
jobPercent(job: TransferSnapshot): number {
|
||||
if (!job.totalBytes) return job.state === 'completed' ? 100 : 0;
|
||||
return Math.min(100, (job.doneBytes / job.totalBytes) * 100);
|
||||
}
|
||||
|
||||
progressLabel(job: TransferSnapshot): string {
|
||||
if (job.state === 'queued') return this.i18n.t('transfer.queuedState');
|
||||
if (job.state === 'scanning') return this.i18n.t('transfer.scanning');
|
||||
if (job.state === 'failed')
|
||||
return `${job.currentFile ? `${job.currentFile} — ` : ''}${job.error ?? this.i18n.t('transfer.failedState')}`;
|
||||
if (job.state === 'cancelled') return this.i18n.t('transfer.cancelled');
|
||||
if (job.state === 'completed')
|
||||
return this.i18n.t('transfer.completedFiles', {
|
||||
count: job.doneFiles,
|
||||
size: this.i18n.size(job.doneBytes),
|
||||
});
|
||||
const speed = this.speedLabel(job);
|
||||
const base = `${job.currentFile} · ${this.i18n.size(job.doneBytes)} / ${this.i18n.size(job.totalBytes)}`;
|
||||
return speed ? `${base} · ${speed}` : base;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user