From 2a7fdd7c89fb772b3b0f3557f89aa3595f8e39be Mon Sep 17 00:00:00 2001 From: 4gray Date: Sat, 3 Oct 2026 18:28:53 +0200 Subject: [PATCH] fix(downloads): download and recording details use the shared chips and cast row Both pages still projected the legacy tag and cast markup whose styles the redesign removed, so chips rendered as plain text and cast photos at their intrinsic size. The tags are meta chips now, the offline cast is the shared credits line plus the cast row (plain entries, there is no actor page offline), and each hero is keyed by the download or recording id so two entries sharing a title decide their layout apart. Co-Authored-By: Claude Fable 5.1 --- .../download-offline-detail.component.html | 105 ++++-------------- .../download-offline-detail.component.scss | 10 -- .../download-offline-detail.component.spec.ts | 6 + .../download-offline-detail.component.ts | 26 ++++- ...wnload-offline-detail.presentation.spec.ts | 27 +++++ .../download-offline-detail.presentation.ts | 13 +++ .../recording-detail.component.html | 40 ++++--- .../recording-detail.component.scss | 3 - .../recording-detail.component.ts | 2 + 9 files changed, 117 insertions(+), 115 deletions(-) diff --git a/libs/portal/downloads/feature/src/lib/offline-detail/download-offline-detail.component.html b/libs/portal/downloads/feature/src/lib/offline-detail/download-offline-detail.component.html index 2af873533..2e1dfd900 100644 --- a/libs/portal/downloads/feature/src/lib/offline-detail/download-offline-detail.component.html +++ b/libs/portal/downloads/feature/src/lib/offline-detail/download-offline-detail.component.html @@ -1,5 +1,6 @@ @if (detail(); as offlineDetail) { - + {{ 'DOWNLOADS.OFFLINE_DETAIL.AVAILABLE' | translate }} - + @if (metadata()?.year; as year) { @if (positiveFinite(year)) { - {{ year }} + {{ year }} } } @if (durationLabel(); as duration) { - {{ duration }} + {{ duration }} } @for (genre of genres(); track genre) { - {{ genre }} + {{ genre }} } @if (metadata()?.rating; as rating) { @if (positiveFinite(rating)) { - + {{ rating }} - + } } @if (metadata()?.status; as status) { @if (status.trim()) { - - {{ status }} - + {{ status }} } } - @if (cast().length) { -
- {{ 'XTREAM.ACTORS' | translate }} -
- @for (person of cast(); track personTrackKey(person)) { - - @if (person.profileUrl) { - - } @else { - - } - {{ - person.name - }} - - } -
-
- } - @if (creators().length) { -
- {{ - 'XTREAM.DIRECTOR' | translate - }} -
- @for ( - person of creators(); - track personTrackKey(person) - ) { - - @if (person.profileUrl) { - - } @else { - - } - {{ - person.name - }} - - } -
-
- } + @if (movieFileSize(); as size) {
@@ -455,6 +386,14 @@ } } +
+ + +
} @else { { expect(text()).toContain('Released'); expect(text()).toContain('Ada Actor'); expect(text()).toContain('Cora Creator'); + // No actor page offline: the shared cast row renders plain entries. + const person = (fixture.nativeElement as HTMLElement).querySelector( + 'app-cast-crew-row .person' + ); + expect(person).not.toBeNull(); + expect(person?.getAttribute('role')).toBeNull(); expect(text()).toContain('2 KB'); expect(text()).not.toContain('{{size}}'); expect(downloads.formatBytes).toHaveBeenCalledWith(2_048); diff --git a/libs/portal/downloads/feature/src/lib/offline-detail/download-offline-detail.component.ts b/libs/portal/downloads/feature/src/lib/offline-detail/download-offline-detail.component.ts index e55c22fc1..63280655c 100644 --- a/libs/portal/downloads/feature/src/lib/offline-detail/download-offline-detail.component.ts +++ b/libs/portal/downloads/feature/src/lib/offline-detail/download-offline-detail.component.ts @@ -12,10 +12,14 @@ import { ActivatedRoute } from '@angular/router'; import { TranslatePipe } from '@ngx-translate/core'; import { DownloadsService, SettingsStore } from '@iptvnator/services'; import { + CastCrewRowComponent, DetailActionsTemplateDirective, + DetailCreditsComponent, DetailMetaTemplateDirective, DetailTagsTemplateDirective, + MetaChipComponent, PortalDetailShellComponent, + scrollToCastCrewRow, } from '@iptvnator/ui/components'; import { DownloadLibraryNavigationService } from '../download-library-navigation.service'; import { DownloadManagerActionsService } from '../download-manager-actions.service'; @@ -32,7 +36,7 @@ import { offlineEpisodeTitle, offlineFileByteCount, offlineMetadataResolutionKey, - offlinePersonTrackKey, + offlinePeopleAsCast, offlinePositiveFinite, offlineSeasonTestId, type OfflineDetailItem, @@ -47,9 +51,12 @@ import { DownloadOfflineSeasonSelectionService } from './download-offline-season @Component({ selector: 'app-download-offline-detail', imports: [ + CastCrewRowComponent, DetailActionsTemplateDirective, + DetailCreditsComponent, DetailMetaTemplateDirective, DetailTagsTemplateDirective, + MetaChipComponent, MatButtonModule, MatIconModule, MatMenuModule, @@ -156,6 +163,21 @@ export class DownloadOfflineDetailComponent { readonly creators = computed(() => boundedOfflinePeople(this.metadata()?.creators) ); + readonly castMembers = computed(() => offlinePeopleAsCast(this.cast())); + readonly creatorMembers = computed(() => + offlinePeopleAsCast(this.creators()) + ); + readonly castNames = computed(() => + this.cast().map((person) => person.name) + ); + readonly creatorNames = computed(() => + this.creators().map((person) => person.name) + ); + /** The hero decides its layout once per download, not per title: two downloads can share one. */ + readonly heroKey = computed(() => { + const downloadId = this.downloadId(); + return downloadId === undefined ? null : `download:${downloadId}`; + }); readonly providerState = this.providerCoordinator.state; readonly canOpenInPortal = computed( () => this.providerState().status === 'available' @@ -208,7 +230,7 @@ export class DownloadOfflineDetailComponent { readonly episodeCoordinate = offlineEpisodeCoordinate; readonly episodeTitle = offlineEpisodeTitle; readonly positiveFinite = offlinePositiveFinite; - readonly personTrackKey = offlinePersonTrackKey; + readonly scrollToCast = scrollToCastCrewRow; constructor() { void this.downloadsService.loadDownloads(); diff --git a/libs/portal/downloads/feature/src/lib/offline-detail/download-offline-detail.presentation.spec.ts b/libs/portal/downloads/feature/src/lib/offline-detail/download-offline-detail.presentation.spec.ts index 697622078..e425fc9f7 100644 --- a/libs/portal/downloads/feature/src/lib/offline-detail/download-offline-detail.presentation.spec.ts +++ b/libs/portal/downloads/feature/src/lib/offline-detail/download-offline-detail.presentation.spec.ts @@ -1,9 +1,36 @@ import { boundedOfflinePeople, + offlinePeopleAsCast, offlinePersonTrackKey, } from './download-offline-detail.presentation'; describe('offline detail people presentation', () => { + it('hands people to the shared cast row with the role as the character', () => { + expect( + offlinePeopleAsCast([ + { + name: 'Ada Actor', + role: 'Mara', + profileUrl: 'https://img/ada.jpg', + }, + { name: 'Cora Creator', tmdbPersonId: 7 }, + ]) + ).toEqual([ + { + name: 'Ada Actor', + character: 'Mara', + profileUrl: 'https://img/ada.jpg', + tmdbPersonId: undefined, + }, + { + name: 'Cora Creator', + character: undefined, + profileUrl: null, + tmdbPersonId: 7, + }, + ]); + }); + it('deduplicates people and supplies unique stable composite track keys', () => { const people = boundedOfflinePeople([ { tmdbPersonId: 7, name: 'Ada Actor', role: 'Lead' }, diff --git a/libs/portal/downloads/feature/src/lib/offline-detail/download-offline-detail.presentation.ts b/libs/portal/downloads/feature/src/lib/offline-detail/download-offline-detail.presentation.ts index de6946bec..a267a8826 100644 --- a/libs/portal/downloads/feature/src/lib/offline-detail/download-offline-detail.presentation.ts +++ b/libs/portal/downloads/feature/src/lib/offline-detail/download-offline-detail.presentation.ts @@ -2,6 +2,7 @@ import type { DownloadItem } from '@iptvnator/services'; import type { DownloadMetadataPerson, DownloadMetadataSnapshot, + TmdbEnrichedCastMember, } from '@iptvnator/shared/interfaces'; import type { DeepReadonly, @@ -144,6 +145,18 @@ export function boundedOfflinePeople( return people; } +/** The shared cast row's shape: the role becomes the character line. */ +export function offlinePeopleAsCast( + people: readonly DownloadMetadataPerson[] +): TmdbEnrichedCastMember[] { + return people.map((person) => ({ + name: person.name, + character: person.role, + profileUrl: person.profileUrl ?? null, + tmdbPersonId: person.tmdbPersonId, + })); +} + export function offlinePersonTrackKey(person: DownloadMetadataPerson): string { return [person.tmdbPersonId ?? 'local', offlinePersonNameKey(person)].join( ':' diff --git a/libs/portal/downloads/feature/src/lib/recording-detail/recording-detail.component.html b/libs/portal/downloads/feature/src/lib/recording-detail/recording-detail.component.html index 11dbb1bf1..89af5fb8a 100644 --- a/libs/portal/downloads/feature/src/lib/recording-detail/recording-detail.component.html +++ b/libs/portal/downloads/feature/src/lib/recording-detail/recording-detail.component.html @@ -1,5 +1,6 @@ @if (item(); as recording) { - + {{ 'DOWNLOADS.REMOVE_FROM_MANAGER' | translate }}
} @else { - + {{ 'DOWNLOADS.RECORDING_BADGE' | translate }} - - {{ recording.channelName }} + + {{ recording.channelName }} @if (durationLabel()) { - {{ durationLabel() }} + {{ durationLabel() }} } @if (sizeLabel()) { - {{ sizeLabel() }} + {{ sizeLabel() }} } - MPEG-TS + MPEG-TS @if (recording.status === 'interrupted') { - + {{ 'DOWNLOADS.STATUS.INTERRUPTED' | translate }} - + } @@ -129,9 +132,7 @@ type="button" data-testid="recording-overflow" [matMenuTriggerFor]="detailMenu" - [attr.aria-label]=" - 'DOWNLOADS.MORE_ACTIONS' | translate - " + [attr.aria-label]="'DOWNLOADS.MORE_ACTIONS' | translate" > more_vert @@ -167,17 +168,22 @@ @for (program of coveredPrograms(); track program.start) {
  • {{ program.title }}
    @if (program.description) { -
    +
    {{ program.description }}
    } diff --git a/libs/portal/downloads/feature/src/lib/recording-detail/recording-detail.component.scss b/libs/portal/downloads/feature/src/lib/recording-detail/recording-detail.component.scss index e2a5dcccf..06022da70 100644 --- a/libs/portal/downloads/feature/src/lib/recording-detail/recording-detail.component.scss +++ b/libs/portal/downloads/feature/src/lib/recording-detail/recording-detail.component.scss @@ -4,9 +4,6 @@ } .recording-detail__badge { - display: inline-flex; - align-items: center; - gap: 4px; background: color-mix(in srgb, var(--mat-sys-error) 18%, transparent); color: var(--mat-sys-error); font-weight: 700; diff --git a/libs/portal/downloads/feature/src/lib/recording-detail/recording-detail.component.ts b/libs/portal/downloads/feature/src/lib/recording-detail/recording-detail.component.ts index 62f7a8b6d..9c64c952a 100644 --- a/libs/portal/downloads/feature/src/lib/recording-detail/recording-detail.component.ts +++ b/libs/portal/downloads/feature/src/lib/recording-detail/recording-detail.component.ts @@ -18,6 +18,7 @@ import { DetailActionsTemplateDirective, DetailMetaTemplateDirective, DetailTagsTemplateDirective, + MetaChipComponent, PortalDetailShellComponent, } from '@iptvnator/ui/components'; import { map } from 'rxjs'; @@ -52,6 +53,7 @@ import { MatButtonModule, MatIcon, MatMenuModule, + MetaChipComponent, PortalDetailShellComponent, TranslatePipe, ],