feat(external-playback-dock): enhance artwork handling with error fallback and dynamic placeholder icons

This commit is contained in:
4gray committed 2026-03-14 23:41:31 +01:00
1 parent 689c7bd949
commit 6f914a6d34
3 files changed
+58 -3

No files matched your search

@@ -4,15 +4,16 @@
[class.external-playback-dock--error]="session().status === 'error'"
>
<div class="external-playback-dock__artwork">
@if (session().thumbnail) {
@if (showArtwork()) {
<img
[src]="session().thumbnail!"
[src]="artworkUrl()"
[alt]="session().title"
loading="lazy"
(error)="onArtworkError()"
/>
} @else {
<div class="external-playback-dock__placeholder">
<mat-icon>play_circle</mat-icon>
<mat-icon>{{ artworkPlaceholderIcon() }}</mat-icon>
</div>
}
</div>
@@ -46,4 +46,28 @@ describe('ExternalPlaybackDockComponent', () => {
expect(closeSpy).toHaveBeenCalled();
expect(buttons).toHaveLength(1);
});
it('falls back to a placeholder icon when artwork fails to load', () => {
fixture.componentRef.setInput('session', {
...session,
thumbnail: 'https://example.com/broken.png',
contentInfo: {
playlistId: 'playlist-1',
contentXtreamId: 42,
contentType: 'vod',
},
});
fixture.detectChanges();
const image = fixture.debugElement.query(By.css('img'));
image.triggerEventHandler('error');
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('img'))).toBeNull();
expect(
fixture.debugElement
.query(By.css('.external-playback-dock__placeholder mat-icon'))
.nativeElement.textContent.trim()
).toBe('movie');
});
});
@@ -2,8 +2,10 @@ import {
ChangeDetectionStrategy,
Component,
computed,
effect,
input,
output,
signal,
} from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatIcon } from '@angular/material/icon';
@@ -22,8 +24,10 @@ export class ExternalPlaybackDockComponent {
readonly compact = input(false);
readonly closeClicked = output<void>();
private readonly artworkFailed = signal(false);
readonly playerLabel = computed(() => this.session().player.toUpperCase());
readonly artworkUrl = computed(() => this.session().thumbnail?.trim() ?? '');
readonly statusLabel = computed(() => {
const session = this.session();
const player = this.playerLabel();
@@ -45,7 +49,33 @@ export class ExternalPlaybackDockComponent {
readonly showSpinner = computed(
() => this.session().status === 'launching'
);
readonly showArtwork = computed(
() => !!this.artworkUrl() && !this.artworkFailed()
);
readonly artworkPlaceholderIcon = computed(() => {
const contentType = this.session().contentInfo?.contentType;
switch (contentType) {
case 'vod':
return 'movie';
case 'episode':
return 'video_library';
default:
return 'live_tv';
}
});
readonly showCloseButton = computed(
() => this.session().canClose && this.session().status !== 'error'
);
constructor() {
effect(() => {
this.artworkUrl();
this.artworkFailed.set(false);
});
}
onArtworkError(): void {
this.artworkFailed.set(true);
}
}