mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
fix(angular): remove template diagnostics warnings (#1011)
* fix(angular): remove template diagnostics warnings * fix(angular): preserve template fallback behavior
This commit is contained in:
1 parent
1bbe8f794e
commit
3d4d2ca9bf
19 files changed
+184
-40
No files matched your search
@@ -3,10 +3,10 @@
|
||||
<div class="account-dialog__eyebrow">
|
||||
<span class="account-dialog__eyebrow-mark"></span>
|
||||
@if (accountInfo(); as info) {
|
||||
{{ info.server_info?.server_protocol || 'Xtream' }}
|
||||
@if (info.server_info?.timezone) {
|
||||
{{ info.server_info.server_protocol || 'Xtream' }}
|
||||
@if (info.server_info.timezone) {
|
||||
<span class="account-dialog__eyebrow-divider"></span>
|
||||
{{ info.server_info?.timezone }}
|
||||
{{ info.server_info.timezone }}
|
||||
}
|
||||
} @else {
|
||||
Xtream
|
||||
@@ -29,7 +29,7 @@
|
||||
class="account-dialog__status-pill"
|
||||
[class.account-dialog__status-pill--active]="isActive()"
|
||||
>
|
||||
{{ info.user_info?.status || '-' }}
|
||||
{{ info.user_info.status || '-' }}
|
||||
</span>
|
||||
|
||||
@if (isTrial()) {
|
||||
@@ -122,7 +122,7 @@
|
||||
<p class="account-panel__eyebrow">
|
||||
{{ 'XTREAM.ACCOUNT_INFO.USER_INFO' | translate }}
|
||||
</p>
|
||||
<h3>{{ info.user_info?.username || '-' }}</h3>
|
||||
<h3>{{ info.user_info.username || '-' }}</h3>
|
||||
</div>
|
||||
|
||||
<dl class="account-detail-list">
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
[title]="info.name"
|
||||
[description]="info.plot"
|
||||
[posterUrl]="info.cover"
|
||||
[backdropUrl]="info.backdrop_path?.[0]"
|
||||
[backdropUrl]="getBackdropUrl(info)"
|
||||
(backClicked)="goBack()"
|
||||
>
|
||||
<ng-container hero-tags>
|
||||
@@ -101,7 +101,7 @@
|
||||
[seasons]="item.episodes"
|
||||
[seriesId]="item.series_id"
|
||||
[playlistId]="currentPlaylistId()"
|
||||
[seriesTitle]="info.name ?? ''"
|
||||
[seriesTitle]="info.name"
|
||||
[playbackPositions]="episodePlaybackPositions()"
|
||||
[xtreamDownloadContext]="xtreamDownloadContext()"
|
||||
[openingEpisodeId]="openingEpisodeId()"
|
||||
|
||||
@@ -278,6 +278,26 @@ describe('SerialDetailsComponent', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('renders series metadata when backdrop_path is absent at runtime', () => {
|
||||
selectedItem.set({
|
||||
series_id: 103,
|
||||
info: {
|
||||
name: 'Series Without Backdrop',
|
||||
plot: 'Series plot',
|
||||
cover: 'cover.jpg',
|
||||
genre: 'Drama',
|
||||
},
|
||||
episodes: {},
|
||||
});
|
||||
|
||||
expect(() => fixture.detectChanges()).not.toThrow();
|
||||
|
||||
const hero = fixture.debugElement.query(
|
||||
By.directive(StubContentHeroComponent)
|
||||
).componentInstance as StubContentHeroComponent;
|
||||
expect(hero.backdropUrl()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('renders quick start as the first episode action and opens that episode', async () => {
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
@@ -292,9 +312,7 @@ describe('SerialDetailsComponent', () => {
|
||||
expect(quickStartButton?.textContent).toContain(
|
||||
'XTREAM.PLAY_FIRST_EPISODE'
|
||||
);
|
||||
expect(quickStartButton?.textContent).toContain(
|
||||
'S01E01 · Episode 1'
|
||||
);
|
||||
expect(quickStartButton?.textContent).toContain('S01E01 · Episode 1');
|
||||
|
||||
quickStartButton?.click();
|
||||
|
||||
@@ -349,9 +367,7 @@ describe('SerialDetailsComponent', () => {
|
||||
expect(quickStartButton?.textContent).toContain(
|
||||
'XTREAM.RESUME_EPISODE'
|
||||
);
|
||||
expect(quickStartButton?.textContent).toContain(
|
||||
'S01E01 · Episode 1'
|
||||
);
|
||||
expect(quickStartButton?.textContent).toContain('S01E01 · Episode 1');
|
||||
|
||||
quickStartButton?.click();
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
PlaybackPositionData,
|
||||
PlayerContentInfo,
|
||||
ResolvedPortalPlayback,
|
||||
XtreamSerieInfo,
|
||||
XtreamSerieEpisode,
|
||||
XtreamSerieDetails,
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
@@ -309,6 +310,10 @@ export class SerialDetailsComponent implements OnInit, OnDestroy {
|
||||
);
|
||||
}
|
||||
|
||||
getBackdropUrl(info: XtreamSerieInfo): string | undefined {
|
||||
return info.backdrop_path?.[0];
|
||||
}
|
||||
|
||||
goBack(): void {
|
||||
this.closeInlinePlayer();
|
||||
this.location.back();
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
[title]="info.name || item?.movie_data?.name"
|
||||
[description]="info.description || info.plot"
|
||||
[posterUrl]="info.movie_image || info.cover_big"
|
||||
[backdropUrl]="info.backdrop_path?.[0]"
|
||||
[backdropUrl]="getBackdropUrl(info)"
|
||||
(backClicked)="goBack()"
|
||||
>
|
||||
<ng-container hero-tags>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { signal } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { of } from 'rxjs';
|
||||
import { Location } from '@angular/common';
|
||||
import { ContentHeroComponent } from '@iptvnator/ui/components';
|
||||
import {
|
||||
PORTAL_EXTERNAL_PLAYBACK,
|
||||
PORTAL_PLAYBACK_POSITIONS,
|
||||
@@ -37,9 +39,9 @@ describe('VodDetailsRouteComponent', () => {
|
||||
const checkFavoriteStatus = jest.fn();
|
||||
const setSelectedItem = jest.fn();
|
||||
const toggleFavorite = jest.fn();
|
||||
const constructVodStreamUrl = jest.fn().mockReturnValue(
|
||||
'http://example.com/movie/650020.mp4'
|
||||
);
|
||||
const constructVodStreamUrl = jest
|
||||
.fn()
|
||||
.mockReturnValue('http://example.com/movie/650020.mp4');
|
||||
const addRecentItem = jest.fn();
|
||||
const downloads = signal([]);
|
||||
const getPlaybackPosition = jest.fn().mockResolvedValue(null);
|
||||
@@ -134,7 +136,9 @@ describe('VodDetailsRouteComponent', () => {
|
||||
provide: PORTAL_PLAYBACK_POSITIONS,
|
||||
useValue: {
|
||||
getPlaybackPosition,
|
||||
savePlaybackPosition: jest.fn().mockResolvedValue(undefined),
|
||||
savePlaybackPosition: jest
|
||||
.fn()
|
||||
.mockResolvedValue(undefined),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -190,7 +194,8 @@ describe('VodDetailsRouteComponent', () => {
|
||||
const host = fixture.nativeElement as HTMLElement;
|
||||
expect(host.textContent).toContain('Die Kühe sind Los! (2004) DE');
|
||||
expect(
|
||||
host.querySelector('[data-testid="xtream-vod-fallback"]')?.textContent
|
||||
host.querySelector('[data-testid="xtream-vod-fallback"]')
|
||||
?.textContent
|
||||
).toContain('XTREAM.DETAIL_FALLBACK.NOTE');
|
||||
expect(
|
||||
host.querySelector('[data-testid="xtream-vod-fallback-status"]')
|
||||
@@ -248,7 +253,35 @@ describe('VodDetailsRouteComponent', () => {
|
||||
|
||||
const host = fixture.nativeElement as HTMLElement;
|
||||
expect(host.textContent).toContain('City of McFarland (2015)');
|
||||
expect(host.querySelector('[data-testid="xtream-vod-fallback"]')).toBeNull();
|
||||
expect(
|
||||
host.querySelector('[data-testid="xtream-vod-fallback"]')
|
||||
).toBeNull();
|
||||
expect(host.querySelector('button.play-btn')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('renders usable metadata when backdrop_path is absent at runtime', () => {
|
||||
selectedItem.set({
|
||||
info: {
|
||||
name: 'Metadata Without Backdrop',
|
||||
description: 'A populated description',
|
||||
movie_image: 'https://example.com/poster.jpg',
|
||||
},
|
||||
movie_data: {
|
||||
stream_id: 678140,
|
||||
name: 'Metadata Without Backdrop',
|
||||
added: '1750671180',
|
||||
category_id: '235',
|
||||
container_extension: 'mkv',
|
||||
custom_sid: null,
|
||||
direct_source: '',
|
||||
},
|
||||
} as unknown as XtreamVodDetails);
|
||||
|
||||
expect(() => fixture.detectChanges()).not.toThrow();
|
||||
|
||||
const hero = fixture.debugElement.query(
|
||||
By.directive(ContentHeroComponent)
|
||||
).componentInstance as ContentHeroComponent;
|
||||
expect(hero.backdropUrl()).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
ResolvedPortalPlayback,
|
||||
XtreamCategory,
|
||||
XtreamVodDetails,
|
||||
XtreamVodInfo,
|
||||
XtreamVodStream,
|
||||
getXtreamVodInfo,
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
@@ -470,6 +471,10 @@ export class VodDetailsRouteComponent implements OnInit, OnDestroy {
|
||||
);
|
||||
}
|
||||
|
||||
getBackdropUrl(info: XtreamVodInfo): string | undefined {
|
||||
return info.backdrop_path?.[0];
|
||||
}
|
||||
|
||||
goBack(): void {
|
||||
this.closeInlinePlayer();
|
||||
this.location.back();
|
||||
|
||||
+2
-2
@@ -2,12 +2,12 @@
|
||||
<div class="channel-dialog__hero-copy">
|
||||
<div class="channel-dialog__eyebrow">
|
||||
<span class="channel-dialog__eyebrow-mark"></span>
|
||||
{{ channel.group?.title || 'M3U' }}
|
||||
{{ channel.group.title || 'M3U' }}
|
||||
</div>
|
||||
|
||||
<div class="channel-dialog__headline-row">
|
||||
<div class="channel-dialog__headline-text">
|
||||
@if (channel.tvg?.logo) {
|
||||
@if (channel.tvg.logo) {
|
||||
<img
|
||||
class="channel-dialog__logo"
|
||||
[src]="channel.tvg.logo"
|
||||
|
||||
+3
-3
@@ -10,14 +10,14 @@
|
||||
i +
|
||||
1 +
|
||||
'. ' +
|
||||
(item.channel?.name || 'CHANNELS.UNNAMED_CHANNEL' | translate)
|
||||
(item.channel.name || 'CHANNELS.UNNAMED_CHANNEL' | translate)
|
||||
"
|
||||
[logo]="item.logo"
|
||||
[showEpg]="shouldShowEpg()"
|
||||
[isRadio]="item.channel?.radio === 'true'"
|
||||
[isRadio]="item.channel.radio === 'true'"
|
||||
[epgProgram]="item.epgProgram"
|
||||
[progressPercentage]="item.progressPercentage"
|
||||
[selected]="activeChannelUrl() === item.channel?.url"
|
||||
[selected]="activeChannelUrl() === item.channel.url"
|
||||
[showDetailsContextMenu]="true"
|
||||
[showAuxActionButton]="true"
|
||||
[auxActionIcon]="'delete'"
|
||||
|
||||
@@ -53,11 +53,11 @@
|
||||
<button
|
||||
class="season-card"
|
||||
(click)="selectSeason(season.key)"
|
||||
[class.season-card--has-episodes]="season.value?.length"
|
||||
[class.season-card--has-episodes]="season.value.length"
|
||||
>
|
||||
<div class="season-card__number">{{ season.key }}</div>
|
||||
<div class="season-card__label">Season</div>
|
||||
@if (season.value?.length) {
|
||||
@if (season.value.length) {
|
||||
<div class="season-card__episodes">
|
||||
{{ season.value.length }}
|
||||
{{
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
</span>
|
||||
}
|
||||
|
||||
@if (item?.desc) {
|
||||
@if (item.desc) {
|
||||
<span
|
||||
(click)="$event.stopPropagation(); showDescription(item)"
|
||||
matListItemMeta
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
>
|
||||
<mat-icon>info</mat-icon>
|
||||
</button>
|
||||
{{ activeChannel?.name }}
|
||||
{{ activeChannel.name }}
|
||||
<div class="spacer"></div>
|
||||
<!-- <button
|
||||
mat-icon-button
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
} @else if (player === 'embedded-mpv') {
|
||||
<app-embedded-mpv-player
|
||||
[playback]="resolvedPlayback()"
|
||||
[recordingFolder]="settings()?.recordingFolder ?? ''"
|
||||
[recordingFolder]="recordingFolder()"
|
||||
(timeUpdate)="timeUpdate.emit($event)"
|
||||
/>
|
||||
} @else {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { StorageMap } from '@ngx-pwa/local-storage';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { of } from 'rxjs';
|
||||
import { of, Subject } from 'rxjs';
|
||||
import { VideoPlayer } from '@iptvnator/shared/interfaces';
|
||||
import { RuntimeCapabilitiesService } from '@iptvnator/services';
|
||||
import type { WebPlayerViewComponent as WebPlayerViewComponentInstance } from './web-player-view.component';
|
||||
@@ -303,6 +303,31 @@ describe('WebPlayerViewComponent', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('renders embedded MPV before settings storage emits', () => {
|
||||
fixture.destroy();
|
||||
|
||||
const pendingSettings = new Subject<unknown>();
|
||||
storageMap.get.mockReturnValue(pendingSettings.asObservable());
|
||||
fixture = TestBed.createComponent(WebPlayerViewComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.componentRef.setInput(
|
||||
'streamUrl',
|
||||
'https://example.com/archive/movie.mkv'
|
||||
);
|
||||
fixture.componentRef.setInput('title', 'Example Movie');
|
||||
fixture.componentRef.setInput(
|
||||
'playerOverride',
|
||||
VideoPlayer.EmbeddedMpv
|
||||
);
|
||||
|
||||
expect(() => fixture.detectChanges()).not.toThrow();
|
||||
|
||||
const player = fixture.debugElement.query(
|
||||
By.directive(StubEmbeddedMpvPlayerComponent)
|
||||
).componentInstance as StubEmbeddedMpvPlayerComponent;
|
||||
expect(player.recordingFolder()).toBe('');
|
||||
});
|
||||
|
||||
it('uses the PWA browser access diagnostic description key outside desktop', () => {
|
||||
const issue = createBrowserAccessDiagnostic();
|
||||
|
||||
|
||||
@@ -80,9 +80,9 @@ export class WebPlayerViewComponent {
|
||||
}>();
|
||||
readonly externalFallbackRequested = output<PlaybackFallbackRequest>();
|
||||
|
||||
settings = toSignal(
|
||||
this.storage.get(STORE_KEY.Settings)
|
||||
) as Signal<Settings>;
|
||||
settings = toSignal(this.storage.get(STORE_KEY.Settings)) as Signal<
|
||||
Settings | undefined
|
||||
>;
|
||||
|
||||
channel!: Channel;
|
||||
player!: VideoPlayer;
|
||||
@@ -122,6 +122,9 @@ export class WebPlayerViewComponent {
|
||||
this.settings()?.player ??
|
||||
VideoPlayer.VideoJs
|
||||
);
|
||||
readonly recordingFolder = computed(
|
||||
() => this.settings()?.recordingFolder ?? ''
|
||||
);
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
|
||||
@@ -2,11 +2,14 @@
|
||||
|
||||
<div class="epg-list">
|
||||
@for (item of epgItems; track $index) {
|
||||
<article class="epg-item" [class.current-program]="isCurrentProgram(item)">
|
||||
<article
|
||||
class="epg-item"
|
||||
[class.current-program]="isCurrentProgram(item)"
|
||||
>
|
||||
<div class="epg-item__meta">
|
||||
<div class="epg-item__time">
|
||||
{{ item.start | date: 'HH:mm' }} -
|
||||
{{ item.stop ?? item.end | date: 'HH:mm' }}
|
||||
{{ getProgramEnd(item) | date: 'HH:mm' }}
|
||||
</div>
|
||||
@if (isCurrentProgram(item)) {
|
||||
<span class="epg-item__badge">Live</span>
|
||||
@@ -41,7 +44,7 @@
|
||||
</article>
|
||||
}
|
||||
|
||||
@if (epgItems?.length === 0) {
|
||||
@if (epgItems.length === 0) {
|
||||
<div class="epg-empty">
|
||||
{{ 'EPG.EPG_NOT_AVAILABLE_CHANNEL_TITLE' | translate }}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { EpgItem } from '@iptvnator/shared/interfaces';
|
||||
import { EpgViewComponent } from './epg-view.component';
|
||||
|
||||
describe('EpgViewComponent', () => {
|
||||
let fixture: ComponentFixture<EpgViewComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [EpgViewComponent, TranslateModule.forRoot()],
|
||||
providers: [
|
||||
{
|
||||
provide: MatDialog,
|
||||
useValue: {
|
||||
open: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(EpgViewComponent);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fixture.destroy();
|
||||
});
|
||||
|
||||
it('renders the end timestamp when stop is absent at runtime', () => {
|
||||
fixture.componentInstance.epgItems = [
|
||||
{
|
||||
id: 'epg-1',
|
||||
epg_id: 'channel-1',
|
||||
title: 'Fallback program',
|
||||
lang: 'en',
|
||||
start: '2026-04-05T11:00:00',
|
||||
end: '2026-04-05T12:30:00',
|
||||
description: 'Program description',
|
||||
channel_id: 'channel-1',
|
||||
start_timestamp: '1775386800',
|
||||
stop_timestamp: '1775392200',
|
||||
} as EpgItem,
|
||||
];
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.nativeElement.textContent).toContain('11:00 - 12:30');
|
||||
});
|
||||
});
|
||||
@@ -20,7 +20,7 @@ export class EpgViewComponent {
|
||||
dialog = inject(MatDialog);
|
||||
|
||||
isCurrentProgram(item: EpgItem): boolean {
|
||||
const end = item.stop ?? item.end;
|
||||
const end = this.getProgramEnd(item);
|
||||
const now = new Date().getTime();
|
||||
const start = new Date(item.start).getTime();
|
||||
const stop = new Date(end).getTime();
|
||||
@@ -30,7 +30,7 @@ export class EpgViewComponent {
|
||||
getProgress(item: EpgItem): number {
|
||||
const now = new Date().getTime();
|
||||
const start = new Date(item.start).getTime();
|
||||
const end = new Date(item.stop ?? item.end).getTime();
|
||||
const end = new Date(this.getProgramEnd(item)).getTime();
|
||||
|
||||
const total = end - start;
|
||||
const current = now - start;
|
||||
@@ -38,6 +38,10 @@ export class EpgViewComponent {
|
||||
return Math.min(Math.max((current / total) * 100, 0), 100);
|
||||
}
|
||||
|
||||
getProgramEnd(item: EpgItem): string {
|
||||
return item.stop ?? item.end;
|
||||
}
|
||||
|
||||
showDetails(item: EpgItem) {
|
||||
this.dialog.open(EpgItemDescriptionComponent, {
|
||||
width: '600px',
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
@if ((items()?.length ?? 0) > 0) {
|
||||
@if (items().length > 0) {
|
||||
<div class="nav-list">
|
||||
@if (statusText()) {
|
||||
<div class="context-inline-status">
|
||||
|
||||
Reference in new issue
Block a user