diff --git a/apps/web/src/app/services/external-playback.service.spec.ts b/apps/web/src/app/services/external-playback.service.spec.ts index 4dd3db10a..059a31c17 100644 --- a/apps/web/src/app/services/external-playback.service.spec.ts +++ b/apps/web/src/app/services/external-playback.service.spec.ts @@ -94,22 +94,57 @@ describe('ExternalPlaybackService', () => { contentType: 'vod', }) ).toBeNull(); + expect(service.visibleSession()).toBeNull(); }); it('delegates close requests for closable sessions', async () => { const session = createSession({ status: 'opened' }); listener?.(session); + closeExternalPlayerSession.mockResolvedValue( + createSession({ + ...session, + status: 'closed', + canClose: false, + updatedAt: '2026-03-07T10:00:10.000Z', + }) + ); + await service.closeActiveSession(); expect(closeExternalPlayerSession).toHaveBeenCalledWith(session.id); + expect(service.visibleSession()).toBeNull(); }); it('can close a specific session directly', async () => { const session = createSession({ id: 'session-2', status: 'playing' }); + closeExternalPlayerSession.mockResolvedValue( + createSession({ + ...session, + status: 'closed', + canClose: false, + updatedAt: '2026-03-07T10:00:12.000Z', + }) + ); + await service.closeSession(session); expect(closeExternalPlayerSession).toHaveBeenCalledWith('session-2'); }); + + it('hides terminal sessions from the dock', () => { + listener?.(createSession({ status: 'closed', canClose: false })); + expect(service.visibleSession()).toBeNull(); + + listener?.( + createSession({ + id: 'session-3', + status: 'error', + error: 'Launch failed', + canClose: false, + }) + ); + expect(service.visibleSession()).toBeNull(); + }); }); diff --git a/apps/web/src/app/services/external-playback.service.ts b/apps/web/src/app/services/external-playback.service.ts index 6eeca0f8b..3cedd66d2 100644 --- a/apps/web/src/app/services/external-playback.service.ts +++ b/apps/web/src/app/services/external-playback.service.ts @@ -10,7 +10,11 @@ export class ExternalPlaybackService { readonly visibleSession = computed(() => { const session = this.activeSession(); - if (!session || session.status === 'closed') { + if ( + !session || + session.status === 'closed' || + session.status === 'error' + ) { return null; } @@ -43,11 +47,43 @@ export class ExternalPlaybackService { async closeSession( session: ExternalPlayerSession | null | undefined ): Promise { - if (!session?.canClose || !window.electron?.closeExternalPlayerSession) { + if (!session) { return; } - await window.electron.closeExternalPlayerSession(session.id); + if (!session.canClose || !window.electron?.closeExternalPlayerSession) { + this.dismissedSessionId.set(session.id); + return; + } + + const previousDismissedSessionId = this.dismissedSessionId(); + this.dismissedSessionId.set(session.id); + + try { + const updatedSession = await window.electron.closeExternalPlayerSession( + session.id + ); + if (updatedSession) { + this.handleSessionUpdate(updatedSession); + return; + } + + this.activeSession.update((current) => + current?.id === session.id + ? { + ...current, + status: 'closed', + canClose: false, + updatedAt: new Date().toISOString(), + } + : current + ); + } catch (error) { + if (this.dismissedSessionId() === session.id) { + this.dismissedSessionId.set(previousDismissedSessionId); + } + throw error; + } } findMatchingSession( diff --git a/libs/ui/components/src/lib/external-playback-dock/external-playback-dock.component.html b/libs/ui/components/src/lib/external-playback-dock/external-playback-dock.component.html index fc47e2859..00aa5cd13 100644 --- a/libs/ui/components/src/lib/external-playback-dock/external-playback-dock.component.html +++ b/libs/ui/components/src/lib/external-playback-dock/external-playback-dock.component.html @@ -53,14 +53,5 @@ } } - diff --git a/libs/ui/components/src/lib/external-playback-dock/external-playback-dock.component.scss b/libs/ui/components/src/lib/external-playback-dock/external-playback-dock.component.scss index 38fdcc55d..e1589007f 100644 --- a/libs/ui/components/src/lib/external-playback-dock/external-playback-dock.component.scss +++ b/libs/ui/components/src/lib/external-playback-dock/external-playback-dock.component.scss @@ -150,10 +150,7 @@ .external-playback-dock__button { border-radius: 999px; -} - -.external-playback-dock__dismiss { - color: var(--mat-sys-on-surface-variant); + white-space: nowrap; } @media (max-width: 720px) { diff --git a/libs/ui/components/src/lib/external-playback-dock/external-playback-dock.component.spec.ts b/libs/ui/components/src/lib/external-playback-dock/external-playback-dock.component.spec.ts index 68dc925be..f5c35531e 100644 --- a/libs/ui/components/src/lib/external-playback-dock/external-playback-dock.component.spec.ts +++ b/libs/ui/components/src/lib/external-playback-dock/external-playback-dock.component.spec.ts @@ -36,17 +36,14 @@ describe('ExternalPlaybackDockComponent', () => { expect(text).toContain('Close player'); }); - it('emits close and dismiss actions', () => { + it('emits a single close action', () => { const closeSpy = jest.fn(); - const dismissSpy = jest.fn(); component.closeClicked.subscribe(closeSpy); - component.dismissClicked.subscribe(dismissSpy); const buttons = fixture.debugElement.queryAll(By.css('button')); buttons[0].nativeElement.click(); - buttons[1].nativeElement.click(); expect(closeSpy).toHaveBeenCalled(); - expect(dismissSpy).toHaveBeenCalled(); + expect(buttons).toHaveLength(1); }); }); diff --git a/libs/ui/components/src/lib/external-playback-dock/external-playback-dock.component.ts b/libs/ui/components/src/lib/external-playback-dock/external-playback-dock.component.ts index 72928da8a..9e4e531da 100644 --- a/libs/ui/components/src/lib/external-playback-dock/external-playback-dock.component.ts +++ b/libs/ui/components/src/lib/external-playback-dock/external-playback-dock.component.ts @@ -22,7 +22,6 @@ export class ExternalPlaybackDockComponent { readonly compact = input(false); readonly closeClicked = output(); - readonly dismissClicked = output(); readonly playerLabel = computed(() => this.session().player.toUpperCase()); readonly statusLabel = computed(() => { diff --git a/libs/workspace/shell/feature/src/lib/workspace-shell/workspace-shell.component.html b/libs/workspace/shell/feature/src/lib/workspace-shell/workspace-shell.component.html index 1f0056177..22c9b2ac5 100644 --- a/libs/workspace/shell/feature/src/lib/workspace-shell/workspace-shell.component.html +++ b/libs/workspace/shell/feature/src/lib/workspace-shell/workspace-shell.component.html @@ -261,7 +261,6 @@ } diff --git a/libs/workspace/shell/feature/src/lib/workspace-shell/workspace-shell.component.ts b/libs/workspace/shell/feature/src/lib/workspace-shell/workspace-shell.component.ts index 7546cd012..923d62e99 100644 --- a/libs/workspace/shell/feature/src/lib/workspace-shell/workspace-shell.component.ts +++ b/libs/workspace/shell/feature/src/lib/workspace-shell/workspace-shell.component.ts @@ -312,10 +312,6 @@ export class WorkspaceShellComponent { this.externalPlayback.activeSession() ); } - - dismissActiveExternalSession(): void { - this.externalPlayback.dismissActiveSession(); - } readonly searchPlaceholder = computed(() => { this.languageTick();