fix(downloads): keep renderer download state global

This commit is contained in:
4gray committed 2026-08-01 16:29:49 +02:00
1 parent 505face7e4
commit 117bd75f01
11 files changed
+102 -23

No files matched your search

@@ -121,6 +121,15 @@
<span class="download-activity-bar__fill"></span>
</span>
}
@if (activeDownloadsCount() > 0) {
<span
class="download-count-badge"
data-test-id="global-download-count"
aria-hidden="true"
>
{{ activeDownloadsCount() }}
</span>
}
<button
type="button"
class="header-shortcut"
@@ -214,6 +214,28 @@
justify-content: center;
}
.download-count-badge {
position: absolute;
top: -2px;
right: -4px;
z-index: 11;
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 18px;
height: 18px;
padding: 0 5px;
border: 1px solid var(--mat-sys-surface-container-low);
border-radius: 999px;
color: var(--app-selection-on-color, var(--mat-sys-on-primary));
background: var(--app-selection-color, var(--mat-sys-primary));
box-shadow: 0 2px 8px var(--app-selection-glow);
font-size: 0.6875rem;
font-weight: 700;
line-height: 1;
pointer-events: none;
}
.download-activity-bar {
position: absolute;
bottom: 2px;
@@ -172,4 +172,32 @@ describe('WorkspaceShellHeaderComponent', () => {
expect(chips).toEqual(['Movies / All Items', 'Loaded channels only']);
});
it('renders the global active download count without duplicating the button name', () => {
fixture.componentRef.setInput('isElectron', true);
fixture.componentRef.setInput('activeDownloadsCount', 3);
fixture.detectChanges();
const badge: HTMLElement | null = fixture.nativeElement.querySelector(
'[data-test-id="global-download-count"]'
);
expect(badge?.textContent?.trim()).toBe('3');
expect(badge?.getAttribute('aria-hidden')).toBe('true');
expect(
fixture.nativeElement.querySelector('.download-activity-bar')
).not.toBeNull();
});
it('does not render a global download badge when no downloads are active', () => {
fixture.componentRef.setInput('isElectron', true);
fixture.componentRef.setInput('activeDownloadsCount', 0);
fixture.detectChanges();
expect(
fixture.nativeElement.querySelector(
'[data-test-id="global-download-count"]'
)
).toBeNull();
});
});
@@ -1,6 +1,7 @@
import {
ChangeDetectionStrategy,
Component,
computed,
ElementRef,
input,
output,
@@ -52,7 +53,10 @@ export class WorkspaceShellHeaderComponent {
readonly isElectron = input(false);
readonly hasNoPlaylists = input(false);
readonly isDownloadsView = input(false);
readonly hasActiveDownloads = input(false);
readonly activeDownloadsCount = input(0);
readonly hasActiveDownloads = computed(
() => this.activeDownloadsCount() > 0
);
/**
* When true the playlist switcher + the "+ Add source" / refresh /
* bulk-action buttons are hidden — those controls scope to a
@@ -376,6 +376,21 @@ describe('WorkspaceShellFacade', () => {
expect(facade.isMacOS).toBe(false);
});
it('exposes the global active download count and derives activity when supported', () => {
downloadsActiveCountSignal.set(3);
expect(facade.activeDownloadsCount()).toBe(3);
expect(facade.hasActiveDownloads()).toBe(true);
});
it('hides the active download count when downloads are unsupported', () => {
runtime.supportsDownloads = false;
downloadsActiveCountSignal.set(3);
expect(facade.activeDownloadsCount()).toBe(0);
expect(facade.hasActiveDownloads()).toBe(false);
});
it('routes dashboard search Enter into the active Xtream playlist search', () => {
const xtreamStore = TestBed.inject(
XtreamStore
@@ -112,8 +112,11 @@ export class WorkspaceShellFacade {
readonly isRefreshingPlaylist = this.header.isRefreshingPlaylist;
readonly headerBulkAction = this.header.headerBulkAction;
readonly playlistSubtitle = this.header.playlistSubtitle;
readonly activeDownloadsCount = computed(() =>
this.supportsDownloads ? this.downloadsService.activeCount() : 0
);
readonly hasActiveDownloads = computed(
() => this.supportsDownloads && this.downloadsService.activeCount() > 0
() => this.activeDownloadsCount() > 0
);
constructor() {
@@ -39,7 +39,7 @@
[isElectron]="facade.isElectron"
[hasNoPlaylists]="facade.hasNoPlaylists()"
[isDownloadsView]="facade.isDownloadsView()"
[hasActiveDownloads]="facade.hasActiveDownloads()"
[activeDownloadsCount]="facade.activeDownloadsCount()"
[isSettingsRoute]="facade.isSettingsRoute()"
(refreshPlaylistRequested)="facade.refreshCurrentPlaylist()"
(searchChanged)="facade.onSearchInput($event)"
@@ -58,7 +58,7 @@ class MockWorkspaceShellHeaderComponent {
readonly isElectron = input(false);
readonly hasNoPlaylists = input(false);
readonly isDownloadsView = input(false);
readonly hasActiveDownloads = input(false);
readonly activeDownloadsCount = input(0);
readonly isSettingsRoute = input(false);
readonly headerBulkAction = input<WorkspaceHeaderBulkAction | null>(null);
readonly searchChanged = output<string>();
@@ -154,7 +154,7 @@ class MockWorkspaceShellFacade {
readonly isRefreshingPlaylist = signal(false);
readonly hasNoPlaylists = signal(false);
readonly isDownloadsView = signal(false);
readonly hasActiveDownloads = signal(false);
readonly activeDownloadsCount = signal(3);
readonly headerBulkAction = signal<WorkspaceHeaderBulkAction | null>(null);
readonly showContextPanel = signal(true);
readonly contextPanel = signal<WorkspaceShellContextPanel>('settings');
@@ -246,6 +246,10 @@ describe('WorkspaceShellComponent', () => {
expect(
fixture.nativeElement.querySelector('app-workspace-shell-header')
).not.toBeNull();
const header = fixture.debugElement.query(
By.directive(MockWorkspaceShellHeaderComponent)
).componentInstance as MockWorkspaceShellHeaderComponent;
expect(header.activeDownloadsCount()).toBe(3);
expect(
fixture.nativeElement.querySelector(
'app-workspace-shell-context-sidebar'