From 169925dbbe2dea3b27a605b417541f364ddb176d Mon Sep 17 00:00:00 2001 From: 4gray Date: Sat, 4 Jul 2026 20:34:36 +0200 Subject: [PATCH] feat(ui): compact episode stills in the list view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a season has genuinely distinct per-episode art (TMDB stills or provider images), list rows show a 96x54 thumbnail with the episode number riding on it instead of the number square. When every episode repeats the same image (providers often send the series poster), the plain number square stays — a column of identical posters is worse than none. Co-Authored-By: Claude Fable 5 --- .../season-container.component.html | 33 ++++++++++- .../season-container.component.scss | 42 ++++++++++++++ .../season-container.component.spec.ts | 55 +++++++++++++++++++ .../season-container.component.ts | 17 ++++++ 4 files changed, 144 insertions(+), 3 deletions(-) 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) {