diff --git a/libs/ui/components/src/lib/season-container/season-container.component.html b/libs/ui/components/src/lib/season-container/season-container.component.html index e9b756e86..9f69f555b 100644 --- a/libs/ui/components/src/lib/season-container/season-container.component.html +++ b/libs/ui/components/src/lib/season-container/season-container.component.html @@ -296,9 +296,36 @@ isEpisodePlayingInline(episode) " > -
- {{ episode.episode_num }} -
+ @if (listThumbnailsEnabled()) { +
+ @if (episodeInfo?.movie_image) { + Episode thumbnail + } @else { + play_circle_outline + } + {{ episode.episode_num }} +
+ } @else { +
+ {{ episode.episode_num }} +
+ }

diff --git a/libs/ui/components/src/lib/season-container/season-container.component.scss b/libs/ui/components/src/lib/season-container/season-container.component.scss index 4427636b3..40736e31f 100644 --- a/libs/ui/components/src/lib/season-container/season-container.component.scss +++ b/libs/ui/components/src/lib/season-container/season-container.component.scss @@ -582,6 +582,48 @@ app-season-tabs { flex-shrink: 0; } + // Compact 16:9 episode still (only when the season has distinct + // per-episode art); the episode number rides on the image. + &__thumb { + position: relative; + flex-shrink: 0; + width: 96px; + height: 54px; + border-radius: 6px; + overflow: hidden; + background: rgba(255, 255, 255, 0.05); + display: flex; + align-items: center; + justify-content: center; + + img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; + } + } + + &__thumb-placeholder { + color: var(--text-muted); + font-size: 22px; + width: 22px; + height: 22px; + } + + &__thumb-number { + position: absolute; + bottom: 3px; + right: 3px; + background: rgba(0, 0, 0, 0.8); + color: white; + font-size: 0.65rem; + font-weight: 600; + line-height: 1.5; + padding: 0 5px; + border-radius: 3px; + } + &__content { flex: 1; min-width: 0; diff --git a/libs/ui/components/src/lib/season-container/season-container.component.spec.ts b/libs/ui/components/src/lib/season-container/season-container.component.spec.ts index de977af09..c2c21f3be 100644 --- a/libs/ui/components/src/lib/season-container/season-container.component.spec.ts +++ b/libs/ui/components/src/lib/season-container/season-container.component.spec.ts @@ -330,6 +330,61 @@ describe('SeasonContainerComponent', () => { ).toBeNull(); }); + it('shows list thumbnails when episode images are distinct', () => { + setRequiredInputs({ + '1': [ + createEpisode({ + id: '101', + info: { plot: 'a', movie_image: 'still-1.jpg' } as never, + }), + createEpisode({ + id: '102', + episode_num: 2, + info: { plot: 'b', movie_image: 'still-2.jpg' } as never, + }), + ], + }); + component.setViewMode('list'); + fixture.detectChanges(); + + expect(component.listThumbnailsEnabled()).toBe(true); + expect( + fixture.nativeElement.querySelectorAll('.episode-list-item__thumb') + .length + ).toBe(2); + expect( + fixture.nativeElement.querySelector('.episode-list-item__number') + ).toBeNull(); + }); + + it('keeps the number square when every episode repeats the same image', () => { + setRequiredInputs({ + '1': [ + createEpisode({ + id: '101', + info: { plot: 'a', movie_image: 'poster.jpg' } as never, + }), + createEpisode({ + id: '102', + episode_num: 2, + info: { plot: 'b', movie_image: 'poster.jpg' } as never, + }), + ], + }); + component.setViewMode('list'); + fixture.detectChanges(); + + expect(component.listThumbnailsEnabled()).toBe(false); + expect( + fixture.nativeElement.querySelector('.episode-list-item__thumb') + ).toBeNull(); + expect( + fixture.nativeElement.querySelectorAll( + '.episode-list-item__number' + ).length + ).toBe(2); + }); + it('renders the season description for the selected season', () => { fixture.componentRef.setInput('seasonDescriptions', { '1': 'Season one overview', diff --git a/libs/ui/components/src/lib/season-container/season-container.component.ts b/libs/ui/components/src/lib/season-container/season-container.component.ts index 0c292fd28..f3b8cc857 100644 --- a/libs/ui/components/src/lib/season-container/season-container.component.ts +++ b/libs/ui/components/src/lib/season-container/season-container.component.ts @@ -155,6 +155,23 @@ export class SeasonContainerComponent implements OnInit { ); private lastAutoSelectKey: string | null = null; + /** + * Show thumbnails in the list view only when episodes have genuinely + * distinct stills (TMDB or per-episode provider art). When every episode + * carries the same image (providers often repeat the series poster) a + * column of identical pictures is worse than the plain number square. + */ + readonly listThumbnailsEnabled = computed(() => { + const episodes = this.selectedSeasonEpisodes(); + const images = episodes + .map((episode) => this.getEpisodeInfo(episode)?.movie_image) + .filter((image): image is string => !!image); + if (images.length === 0) { + return false; + } + return episodes.length === 1 || new Set(images).size > 1; + }); + readonly selectedSeasonDescription = computed(() => { const selected = this.selectedSeason(); if (!selected) {