From 4ab8915483db92b192ccdcffd3adab4a3052491d Mon Sep 17 00:00:00 2001 From: 4gray Date: Fri, 22 May 2026 02:58:12 +0300 Subject: [PATCH] chore(web): enable strict TypeScript mode --- apps/web/src/app/app.component.ts | 16 +- ...edded-mpv-player-recording-message.spec.ts | 8 +- apps/web/src/app/services/electron.service.ts | 14 +- apps/web/src/app/services/pwa.service.ts | 37 +++-- apps/web/src/app/services/settings.service.ts | 20 +-- .../app/settings/settings.component.spec.ts | 3 +- .../src/app/settings/settings.component.ts | 81 +++++++--- apps/web/src/typings.d.ts | 3 +- apps/web/tsconfig.json | 2 +- .../add-playlist-dialog.component.html | 2 +- .../stalker-portal-import.component.ts | 14 +- .../xtream-code-import.component.html | 2 +- .../xtream-code-import.component.ts | 4 +- .../playlist-switcher.component.ts | 18 ++- .../recent-playlists.component.ts | 2 +- .../feature/src/lib/downloads.component.ts | 25 ++- .../unified-favorites-data.service.ts | 2 +- .../collection/unified-recent-data.service.ts | 2 +- .../category-view.component.html | 2 +- .../favorites-layout.component.ts | 11 +- .../grid-list/grid-list.component.ts | 14 +- .../playlist-error-view.component.ts | 4 +- .../navigation/workspace-portal-navigation.ts | 19 ++- .../src/lib/stalker-session.service.ts | 16 +- .../stalker-catalog-detail.component.ts | 10 +- .../src/lib/stalker-catalog-facade.service.ts | 38 ++--- .../stalker-collection-detail.component.ts | 2 +- .../stalker-favorites-button.component.ts | 13 +- .../stalker-inline-detail.component.html | 4 +- .../stalker-live-stream-layout.component.ts | 52 +++++-- .../stalker-search.component.html | 6 +- .../stalker-search.component.ts | 47 +++--- .../stalker-series-view.component.html | 26 ++-- .../stalker-series-view.component.ts | 7 +- .../lib/stalker-vod-playback-controller.ts | 4 +- .../electron-xtream-data-source.ts | 2 +- .../data-sources/pwa-xtream-data-source.ts | 4 +- .../stores/features/with-player.feature.ts | 14 +- .../data-access/src/lib/with-recent-items.ts | 10 +- .../portal-channels-list.component.html | 2 +- .../recently-added.component.html | 6 +- .../search-results.component.ts | 21 ++- .../serial-details.component.html | 39 ++--- .../serial-details.component.ts | 28 +++- .../vod-details-route.component.html | 33 ++-- .../vod-details-route.component.ts | 143 +++++++++++------ .../lib/xtream-collection-detail.component.ts | 2 +- libs/services/src/lib/data.service.ts | 26 +++- .../all-channels-view.component.html | 2 +- .../season-container.component.html | 146 +++++++++++++----- .../embedded-mpv-session-controller.ts | 2 +- .../src/lib/embedded-mpv-player/index.ts | 3 + .../lib/vjs-player/vjs-player.component.ts | 26 ++-- ...space-context-category-view.component.html | 2 +- tsconfig.base.json | 3 + 55 files changed, 675 insertions(+), 369 deletions(-) create mode 100644 libs/ui/playback/src/lib/embedded-mpv-player/index.ts diff --git a/apps/web/src/app/app.component.ts b/apps/web/src/app/app.component.ts index efda3665f..2183b900c 100644 --- a/apps/web/src/app/app.component.ts +++ b/apps/web/src/app/app.component.ts @@ -49,14 +49,16 @@ export class AppComponent implements OnInit { private readonly DEFAULT_LANG = Language.ENGLISH; constructor() { + const electronProcess = this.dataService.remote?.process; if ( - ((this.dataService.isElectron && - this.dataService?.remote?.process.platform === 'linux') || - this.dataService?.remote?.process.platform === 'win32') && - this.dataService.remote.process.argv.length > 2 + this.dataService.isElectron && + electronProcess && + (electronProcess.platform === 'linux' || + electronProcess.platform === 'win32') && + electronProcess.argv.length > 2 ) { - const filePath = this.dataService.remote.process.argv.find( - (filepath) => + const filePath = electronProcess.argv.find( + (filepath: string) => filepath.endsWith('.m3u') || filepath.endsWith('.m3u8') ); if (filePath) { @@ -102,7 +104,7 @@ export class AppComponent implements OnInit { */ initSettings(): void { this.settingsService - .getValueFromLocalStorage(STORE_KEY.Settings) + .getValueFromLocalStorage(STORE_KEY.Settings) .subscribe((settings: Settings) => { if (settings && Object.keys(settings).length > 0) { // No need to send settings to Electron on init diff --git a/apps/web/src/app/embedded-mpv-player-recording-message.spec.ts b/apps/web/src/app/embedded-mpv-player-recording-message.spec.ts index d8932fac6..3e72d8d58 100644 --- a/apps/web/src/app/embedded-mpv-player-recording-message.spec.ts +++ b/apps/web/src/app/embedded-mpv-player-recording-message.spec.ts @@ -2,9 +2,11 @@ import { Component, signal } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { ResolvedPortalPlayback } from '@iptvnator/shared/interfaces'; -import { EmbeddedMpvOverlayVisibilityService } from '../../../../libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-overlay-visibility.service'; -import { EmbeddedMpvPlayerComponent } from '../../../../libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-player.component'; -import { EmbeddedMpvSessionController } from '../../../../libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-session-controller'; +import { + EmbeddedMpvOverlayVisibilityService, + EmbeddedMpvPlayerComponent, + EmbeddedMpvSessionController, +} from '@iptvnator/ui/playback/embedded-mpv-player'; @Component({ imports: [EmbeddedMpvPlayerComponent], diff --git a/apps/web/src/app/services/electron.service.ts b/apps/web/src/app/services/electron.service.ts index 0adccd9e4..2ec59d00c 100644 --- a/apps/web/src/app/services/electron.service.ts +++ b/apps/web/src/app/services/electron.service.ts @@ -117,7 +117,7 @@ export class ElectronService extends DataService { payload?: unknown ): Promise { if (type === PLAYLIST_PARSE_BY_URL) { - this.fetchM3uPlaylistFromUrl(payload); + this.fetchM3uPlaylistFromUrl(payload as Partial); return undefined as T; } @@ -156,7 +156,7 @@ export class ElectronService extends DataService { data.url, data.title ?? '', data.thumbnail ?? '', - data['user-agent'] ?? undefined, + data['user-agent'] ?? '', data.referer ?? undefined, data.origin ?? undefined, data.contentInfo, @@ -185,7 +185,7 @@ export class ElectronService extends DataService { data.url, data.title ?? '', data.thumbnail ?? '', - data['user-agent'] ?? undefined, + data['user-agent'] ?? '', data.referer ?? undefined, data.origin ?? undefined, data.contentInfo, @@ -219,7 +219,7 @@ export class ElectronService extends DataService { this.translateService.instant( 'HOME.PLAYLISTS.AUTO_REFRESH_UPDATE_SUCCESS' ), - null, + undefined, { duration: 2000 } ); return playlists as T; @@ -266,6 +266,10 @@ export class ElectronService extends DataService { } private async fetchM3uPlaylistFromUrl(payload: Partial) { + if (!payload.url) { + return; + } + const title = payload.title?.trim() || undefined; window.electron @@ -353,7 +357,7 @@ export class ElectronService extends DataService { this.translateService.instant( 'HOME.PLAYLISTS.PLAYLIST_UPDATE_SUCCESS' ), - null, + undefined, { duration: 2000 } ); } catch (error: unknown) { diff --git a/apps/web/src/app/services/pwa.service.ts b/apps/web/src/app/services/pwa.service.ts index 25e2af2d5..c7a853b51 100644 --- a/apps/web/src/app/services/pwa.service.ts +++ b/apps/web/src/app/services/pwa.service.ts @@ -5,7 +5,14 @@ import { SwUpdate } from '@angular/service-worker'; import { Store } from '@ngrx/store'; import { TranslateService } from '@ngx-translate/core'; import { PlaylistActions } from '@iptvnator/m3u-state'; -import { catchError, firstValueFrom, from, switchMap, throwError } from 'rxjs'; +import { + catchError, + firstValueFrom, + from, + Observable, + switchMap, + throwError, +} from 'rxjs'; import { DataService } from '@iptvnator/services'; import { ERROR, @@ -108,12 +115,12 @@ export class PwaService extends DataService { */ sendIpcEvent(type: string, payload?: unknown): T { if (type === PLAYLIST_PARSE_BY_URL) { - this.fetchFromUrl(payload); + this.fetchFromUrl(payload as Partial); return undefined as T; } if (type === PLAYLIST_UPDATE) { - this.refreshPlaylist(payload); + this.refreshPlaylist(payload as Partial); return undefined as T; } @@ -138,6 +145,12 @@ export class PwaService extends DataService { } refreshPlaylist(payload: Partial) { + if (!payload.url || !payload.id) { + return; + } + + const playlistId = payload.id; + this.getPlaylistFromUrl(payload.url) .pipe( catchError((error) => { @@ -155,7 +168,7 @@ export class PwaService extends DataService { this.store.dispatch( PlaylistActions.updatePlaylist({ playlist, - playlistId: payload.id, + playlistId, }) ); @@ -163,7 +176,7 @@ export class PwaService extends DataService { this.translateService.instant( 'HOME.PLAYLISTS.PLAYLIST_UPDATE_SUCCESS' ), - null, + undefined, { duration: 2000 } ); }); @@ -193,13 +206,19 @@ export class PwaService extends DataService { * @param payload playlist payload */ fetchFromUrl(payload: Partial): void { + if (!payload.url) { + return; + } + const title = payload.title?.trim() || undefined; this.getPlaylistFromUrl(payload.url) .pipe( catchError((error) => { this.snackBar.open( - this.getErrorMessageByStatusCode(error.status), + this.getErrorMessageByStatusCode( + this.extractHttpStatusCode(error) + ), 'Close', { duration: 5000, @@ -226,7 +245,7 @@ export class PwaService extends DataService { }); } - getErrorMessageByStatusCode(status: number) { + getErrorMessageByStatusCode(status: number | null | undefined) { let messageKey = 'HOME.URL_UPLOAD.ERROR_FETCH_FAILED'; switch (status) { case 413: @@ -524,10 +543,10 @@ export class PwaService extends DataService { } } - getPlaylistFromUrl(url: string) { + getPlaylistFromUrl(url: string): Observable { return from(this.getProviderTargetId(url)).pipe( switchMap((targetId) => - this.http.get(`${this.corsProxyUrl}/parse`, { + this.http.get(`${this.corsProxyUrl}/parse`, { params: { targetId }, }) ) diff --git a/apps/web/src/app/services/settings.service.ts b/apps/web/src/app/services/settings.service.ts index da980f203..a75978e3e 100644 --- a/apps/web/src/app/services/settings.service.ts +++ b/apps/web/src/app/services/settings.service.ts @@ -115,7 +115,10 @@ export class SettingsService { const mediaQuery = this.systemThemeMediaQuery as LegacyMediaQueryList; if (mediaQuery.addEventListener) { - mediaQuery.addEventListener('change', this.systemThemeChangeHandler); + mediaQuery.addEventListener( + 'change', + this.systemThemeChangeHandler + ); } else { mediaQuery.addListener?.(this.systemThemeChangeHandler); } @@ -147,8 +150,8 @@ export class SettingsService { * @param key key to get * @returns returns the value of the given key */ - getValueFromLocalStorage(key: STORE_KEY) { - return this.storage.get(key); + getValueFromLocalStorage(key: STORE_KEY): Observable { + return this.storage.get(key) as Observable; } /** @@ -175,9 +178,9 @@ export class SettingsService { */ getAppVersion() { return this.http - .get<{ created_at: string; name: string }[]>( - 'https://api.github.com/repos/4gray/iptvnator/releases' - ) + .get< + { created_at: string; name: string }[] + >('https://api.github.com/repos/4gray/iptvnator/releases') .pipe( map((response) => { // Filter out pre-release versions (beta, alpha, rc, etc.) @@ -215,10 +218,7 @@ export class SettingsService { * @param latestVersion latest stable version from GitHub * @returns true if current version is outdated */ - isVersionOutdated( - currentVersion: string, - latestVersion: string - ): boolean { + isVersionOutdated(currentVersion: string, latestVersion: string): boolean { const cleanCurrent = parseVersion(currentVersion); const cleanLatest = parseVersion(latestVersion); diff --git a/apps/web/src/app/settings/settings.component.spec.ts b/apps/web/src/app/settings/settings.component.spec.ts index b4f20bdf0..096db46d9 100644 --- a/apps/web/src/app/settings/settings.component.spec.ts +++ b/apps/web/src/app/settings/settings.component.spec.ts @@ -494,7 +494,8 @@ describe('SettingsComponent', () => { .some((player) => player.id === VideoPlayer.EmbeddedMpv) ).toBe(false); - resolveSupport!({ + expect(resolveSupport).toBeDefined(); + resolveSupport({ supported: true, platform: 'darwin', }); diff --git a/apps/web/src/app/settings/settings.component.ts b/apps/web/src/app/settings/settings.component.ts index e2bcb8848..9ea9173e9 100644 --- a/apps/web/src/app/settings/settings.component.ts +++ b/apps/web/src/app/settings/settings.component.ts @@ -12,6 +12,7 @@ import { import { FormArray, FormBuilder, + FormControl, ReactiveFormsModule, Validators, } from '@angular/forms'; @@ -48,6 +49,7 @@ import { CoverSize, Language, normalizeExternalPlayerArguments, + Settings, StartupBehavior, StreamFormat, Theme, @@ -162,10 +164,10 @@ export class SettingsComponent implements OnInit, OnDestroy { ]); /** Current version of the app */ - version: string; + version = ''; /** Update message to show */ - updateMessage: string; + updateMessage = ''; /** EPG availability flag */ epgAvailable$ = this.store.select(selectIsEpgAvailable); @@ -178,7 +180,9 @@ export class SettingsComponent implements OnInit, OnDestroy { /** Settings form object */ settingsForm = this.formBuilder.group({ player: [VideoPlayer.VideoJs], - ...(this.isDesktop ? { epgUrl: new FormArray([]) } : {}), + ...(this.isDesktop + ? { epgUrl: new FormArray>([]) } + : {}), streamFormat: StreamFormat.M3u8StreamFormat, openStreamOnDoubleClick: false, language: Language.ENGLISH, @@ -468,21 +472,7 @@ export class SettingsComponent implements OnInit, OnDestroy { * the indexed db store */ onSubmit(): void { - const settings = { - ...this.settingsForm.value, - mpvPlayerPath: this.normalizeExternalPlayerPath( - this.settingsForm.value.mpvPlayerPath - ), - vlcPlayerPath: this.normalizeExternalPlayerPath( - this.settingsForm.value.vlcPlayerPath - ), - mpvPlayerArguments: normalizeExternalPlayerArguments( - this.settingsForm.value.mpvPlayerArguments - ), - vlcPlayerArguments: normalizeExternalPlayerArguments( - this.settingsForm.value.vlcPlayerArguments - ), - }; + const settings = this.createSettingsFromFormValue(); this.settingsStore.updateSettings(settings).then(() => { this.applyChangedSettings(); @@ -505,6 +495,48 @@ export class SettingsComponent implements OnInit, OnDestroy { return playerPath?.trim() ?? ''; } + private createSettingsFromFormValue(): Settings { + const value = this.settingsForm.value; + const epgUrl = Array.isArray(value.epgUrl) + ? value.epgUrl.filter( + (url): url is string => typeof url === 'string' + ) + : []; + + return { + player: value.player ?? VideoPlayer.VideoJs, + streamFormat: value.streamFormat ?? StreamFormat.M3u8StreamFormat, + openStreamOnDoubleClick: value.openStreamOnDoubleClick ?? false, + language: value.language ?? Language.ENGLISH, + showCaptions: value.showCaptions ?? false, + showDashboard: value.showDashboard ?? true, + startupBehavior: value.startupBehavior ?? StartupBehavior.FirstView, + showExternalPlaybackBar: value.showExternalPlaybackBar ?? true, + theme: value.theme ?? Theme.SystemTheme, + mpvPlayerPath: this.normalizeExternalPlayerPath( + value.mpvPlayerPath + ), + mpvPlayerArguments: normalizeExternalPlayerArguments( + value.mpvPlayerArguments + ), + mpvReuseInstance: value.mpvReuseInstance ?? false, + vlcPlayerPath: this.normalizeExternalPlayerPath( + value.vlcPlayerPath + ), + vlcPlayerArguments: normalizeExternalPlayerArguments( + value.vlcPlayerArguments + ), + vlcReuseInstance: value.vlcReuseInstance ?? false, + remoteControl: value.remoteControl ?? false, + remoteControlPort: Number(value.remoteControlPort ?? 8765), + recordingFolder: value.recordingFolder ?? '', + coverSize: value.coverSize ?? 'medium', + epgUrl, + preferUploadedEpgOverXtream: + value.preferUploadedEpgOverXtream ?? false, + }; + } + /** * Applies the changed settings to the app */ @@ -516,14 +548,19 @@ export class SettingsComponent implements OnInit, OnDestroy { if (!Array.isArray(epgUrls)) { epgUrls = [epgUrls]; } - epgUrls = epgUrls.filter((url) => url !== ''); - if (epgUrls.length > 0) { + const validEpgUrls = epgUrls.filter( + (url): url is string => + typeof url === 'string' && url !== '' + ); + if (validEpgUrls.length > 0) { // Fetch all EPG URLs at once - this.epgService.fetchEpg(epgUrls); + this.epgService.fetchEpg(validEpgUrls); } } } - this.translate.use(this.settingsForm.value.language); + this.translate.use( + this.settingsForm.value.language ?? Language.ENGLISH + ); this.settingsService.changeTheme( this.settingsForm.value.theme ?? Theme.SystemTheme ); diff --git a/apps/web/src/typings.d.ts b/apps/web/src/typings.d.ts index 34d49ea50..1297fc31f 100644 --- a/apps/web/src/typings.d.ts +++ b/apps/web/src/typings.d.ts @@ -19,6 +19,7 @@ import { PlaylistRefreshPayload, PortalDebugEvent, ResolvedPortalPlayback, + Settings, XtreamBackupFavoriteItem, XtreamBackupHiddenCategory, XtreamBackupRecentlyViewedItem, @@ -119,7 +120,7 @@ declare global { searchTerm: string, limit?: number ) => Promise; - updateSettings: (settings: JsonObject) => Promise; + updateSettings: (settings: Partial) => Promise; getAiSettings: () => Promise<{ aiProvider: string; aiModelName: string; diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index b17319d3b..83086f821 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -13,7 +13,7 @@ "typeRoots": ["../../node_modules/@types"], "lib": ["es2018", "es2017", "es2016", "es2015", "dom"], "resolveJsonModule": true, - "strict": false, + "strict": true, "skipLibCheck": true, "useDefineForClassFields": false }, diff --git a/libs/playlist/import/feature/src/lib/add-playlist-dialog/add-playlist-dialog.component.html b/libs/playlist/import/feature/src/lib/add-playlist-dialog/add-playlist-dialog.component.html index c78c7c66f..6bb2389e3 100644 --- a/libs/playlist/import/feature/src/lib/add-playlist-dialog/add-playlist-dialog.component.html +++ b/libs/playlist/import/feature/src/lib/add-playlist-dialog/add-playlist-dialog.component.html @@ -109,7 +109,7 @@ [disabled]="!textImport()?.textForm?.valid" (click)=" textImport()?.textAdded?.emit( - textImport()?.textForm?.value?.text + textImport()?.textForm?.value?.text ?? undefined ) " > diff --git a/libs/playlist/import/feature/src/lib/stalker-portal-import/stalker-portal-import.component.ts b/libs/playlist/import/feature/src/lib/stalker-portal-import/stalker-portal-import.component.ts index a812afa05..266a7b40b 100644 --- a/libs/playlist/import/feature/src/lib/stalker-portal-import/stalker-portal-import.component.ts +++ b/libs/playlist/import/feature/src/lib/stalker-portal-import/stalker-portal-import.component.ts @@ -112,11 +112,11 @@ export class StalkerPortalImportComponent { const isFullStalkerPortal = this.isFullStalkerPortalUrl(originalUrl); const stalkerIdentity = normalizeStalkerPortalIdentity({ - serialNumber: formValue.serialNumber, - deviceId1: formValue.deviceId1, - deviceId2: formValue.deviceId2, - signature1: formValue.signature1, - signature2: formValue.signature2, + serialNumber: formValue.serialNumber ?? undefined, + deviceId1: formValue.deviceId1 ?? undefined, + deviceId2: formValue.deviceId2 ?? undefined, + signature1: formValue.signature1 ?? undefined, + signature2: formValue.signature2 ?? undefined, }); let stalkerToken: string | undefined; @@ -151,7 +151,7 @@ export class StalkerPortalImportComponent { ); this.snackBar.open( `Portal validated. Expires: ${expireDate.toLocaleDateString()}`, - null, + undefined, { duration: 3000 } ); } @@ -162,7 +162,7 @@ export class StalkerPortalImportComponent { ); this.snackBar.open( 'Failed to authenticate with portal. Please check URL and MAC address.', - null, + undefined, { duration: 5000 } ); this.isLoading.set(false); diff --git a/libs/playlist/import/feature/src/lib/xtream-code-import/xtream-code-import.component.html b/libs/playlist/import/feature/src/lib/xtream-code-import/xtream-code-import.component.html index 897bb428c..dc30f54d8 100644 --- a/libs/playlist/import/feature/src/lib/xtream-code-import/xtream-code-import.component.html +++ b/libs/playlist/import/feature/src/lib/xtream-code-import/xtream-code-import.component.html @@ -14,7 +14,7 @@ type="text" id="serverUrl" formControlName="serverUrl" - (input)="extractParams(form.value.serverUrl)" + (input)="extractParams(form.value.serverUrl ?? '')" /> {{ 'HOME.XTREAM_PLAYLIST.URL_VALIDATION_ERROR' | translate diff --git a/libs/playlist/import/feature/src/lib/xtream-code-import/xtream-code-import.component.ts b/libs/playlist/import/feature/src/lib/xtream-code-import/xtream-code-import.component.ts index c0a850697..ef3f7e670 100644 --- a/libs/playlist/import/feature/src/lib/xtream-code-import/xtream-code-import.component.ts +++ b/libs/playlist/import/feature/src/lib/xtream-code-import/xtream-code-import.component.ts @@ -155,8 +155,8 @@ export class XtreamCodeImportComponent { extractParams(urlAsString: string): void { if ( - this.form.get('username').value !== '' || - this.form.get('password').value !== '' + this.form.get('username')?.value !== '' || + this.form.get('password')?.value !== '' ) return; try { diff --git a/libs/playlist/shared/ui/src/lib/playlist-switcher/playlist-switcher.component.ts b/libs/playlist/shared/ui/src/lib/playlist-switcher/playlist-switcher.component.ts index 44775c432..fe3dae062 100644 --- a/libs/playlist/shared/ui/src/lib/playlist-switcher/playlist-switcher.component.ts +++ b/libs/playlist/shared/ui/src/lib/playlist-switcher/playlist-switcher.component.ts @@ -411,8 +411,16 @@ export class PlaylistSwitcherComponent { this.portalStatusAbortController = controller; const xtreamPlaylists = playlists.filter( - (playlist) => - playlist.serverUrl && playlist.username && playlist.password + ( + playlist + ): playlist is PlaylistMeta & { + serverUrl: string; + username: string; + password: string; + } => + Boolean( + playlist.serverUrl && playlist.username && playlist.password + ) ); if (xtreamPlaylists.length === 0) { return; @@ -422,7 +430,11 @@ export class PlaylistSwitcherComponent { // 'checking' in a single signal write so the UI flips from blank → // cached/checking dots in one render, not one per playlist. const next = new Map(this.portalStatuses()); - const toFetch: PlaylistMeta[] = []; + const toFetch: (PlaylistMeta & { + serverUrl: string; + username: string; + password: string; + })[] = []; for (const playlist of xtreamPlaylists) { const cached = this.portalStatusService.getCachedStatus( playlist.serverUrl, diff --git a/libs/playlist/shared/ui/src/lib/recent-playlists/recent-playlists.component.ts b/libs/playlist/shared/ui/src/lib/recent-playlists/recent-playlists.component.ts index 7743a3277..3466dbe05 100644 --- a/libs/playlist/shared/ui/src/lib/recent-playlists/recent-playlists.component.ts +++ b/libs/playlist/shared/ui/src/lib/recent-playlists/recent-playlists.component.ts @@ -460,7 +460,7 @@ export class RecentPlaylistsComponent { this.translate.instant( 'HOME.PLAYLISTS.PLAYLIST_UPDATE_SUCCESS' ), - null, + undefined, { duration: 2000 } ); } catch (error) { diff --git a/libs/portal/downloads/feature/src/lib/downloads.component.ts b/libs/portal/downloads/feature/src/lib/downloads.component.ts index 25edc527c..6fd9b69e1 100644 --- a/libs/portal/downloads/feature/src/lib/downloads.component.ts +++ b/libs/portal/downloads/feature/src/lib/downloads.component.ts @@ -15,7 +15,7 @@ import { MatTooltip } from '@angular/material/tooltip'; import { ActivatedRoute, Router } from '@angular/router'; import { TranslatePipe, TranslateService } from '@ngx-translate/core'; import { DialogService } from '@iptvnator/ui/components'; -import { firstValueFrom, map } from 'rxjs'; +import { firstValueFrom, map, startWith } from 'rxjs'; import { DatabaseService, type DownloadItem, @@ -83,16 +83,22 @@ export class DownloadsComponent { readonly searchTerm = queryParamSignal(this.route, 'q', (value) => (value ?? '').trim().toLowerCase() ); - readonly playlists = toSignal(this.playlistsService.getAllPlaylists(), { - initialValue: null as Playlist[] | null, - }); + readonly playlists = toSignal( + this.playlistsService.getAllPlaylists().pipe(startWith(null)), + { + initialValue: null as Playlist[] | null, + } + ); readonly playlistsLoaded = computed(() => this.playlists() !== null); readonly playlistItems = computed(() => this.playlists() ?? []); readonly hasNoPlaylists = computed( () => this.playlistsLoaded() && this.playlistItems().length === 0 ); readonly skeletonItems = Array.from({ length: 6 }, (_, index) => index); - readonly skeletonActionSlots = Array.from({ length: 4 }, (_, index) => index); + readonly skeletonActionSlots = Array.from( + { length: 4 }, + (_, index) => index + ); addPlaylist(): void { this.shellActions.openAddPlaylistDialog(); @@ -275,7 +281,9 @@ export class DownloadsComponent { async reveal(item: DownloadItem) { if (item.filePath) { - const result = await this.downloadsService.revealFile(item.filePath); + const result = await this.downloadsService.revealFile( + item.filePath + ); if (!result.success) { this.handleFileActionError(result.error); } @@ -309,7 +317,10 @@ export class DownloadsComponent { } hasPoster(item: DownloadItem): boolean { - return !!item.posterUrl && !this.failedPosterKeys()[this.getPosterKey(item)]; + return ( + !!item.posterUrl && + !this.failedPosterKeys()[this.getPosterKey(item)] + ); } markPosterFailed(item: DownloadItem): void { diff --git a/libs/portal/shared/data-access/src/lib/collection/unified-favorites-data.service.ts b/libs/portal/shared/data-access/src/lib/collection/unified-favorites-data.service.ts index ee40affa2..2ab10870e 100644 --- a/libs/portal/shared/data-access/src/lib/collection/unified-favorites-data.service.ts +++ b/libs/portal/shared/data-access/src/lib/collection/unified-favorites-data.service.ts @@ -32,7 +32,7 @@ import { const GLOBAL_FAVORITES_ORDER_KEY = 'global-favorites-channel-order-v1'; -type PlaylistWithChannels = Playlist & { +type PlaylistWithChannels = Omit & { readonly playlist?: { readonly items?: Channel[] }; }; type StalkerPortalFavoriteItem = StalkerPortalItem & { diff --git a/libs/portal/shared/data-access/src/lib/collection/unified-recent-data.service.ts b/libs/portal/shared/data-access/src/lib/collection/unified-recent-data.service.ts index 163c59653..cb743c8e4 100644 --- a/libs/portal/shared/data-access/src/lib/collection/unified-recent-data.service.ts +++ b/libs/portal/shared/data-access/src/lib/collection/unified-recent-data.service.ts @@ -31,7 +31,7 @@ import { XtreamContentItem, } from '@iptvnator/portal/xtream/data-access'; -type PlaylistWithChannels = Playlist & { +type PlaylistWithChannels = Omit & { readonly playlist?: { readonly items?: Channel[] }; }; diff --git a/libs/portal/shared/ui/src/lib/components/category-view/category-view.component.html b/libs/portal/shared/ui/src/lib/components/category-view/category-view.component.html index 10dfcd771..cbce1c381 100644 --- a/libs/portal/shared/ui/src/lib/components/category-view/category-view.component.html +++ b/libs/portal/shared/ui/src/lib/components/category-view/category-view.component.html @@ -1,4 +1,4 @@ -@if (items()?.length > 0) { +@if (items().length > 0) { - @if (showPaginator() && items()?.length > 0) { + @if (showPaginator() && items().length > 0) { (); - readonly isLoading = input(); + readonly items = input([]); + readonly isLoading = input(false); readonly showPaginator = input(true); readonly searchTerm = input(''); readonly itemClicked = output(); readonly pageChange = output(); - readonly pageIndex = input(); - readonly totalPages = input(); - readonly limit = input(); - readonly pageSizeOptions = input(); + readonly pageIndex = input(0); + readonly totalPages = input(0); + readonly limit = input(25); + readonly pageSizeOptions = input([]); protected readonly resolveRating = resolveGridRating; protected readonly hasActiveSearch = computed( () => (this.searchTerm() ?? '').trim().length > 0 diff --git a/libs/portal/shared/ui/src/lib/components/playlist-error-view/playlist-error-view.component.ts b/libs/portal/shared/ui/src/lib/components/playlist-error-view/playlist-error-view.component.ts index 387f32887..864100c3a 100644 --- a/libs/portal/shared/ui/src/lib/components/playlist-error-view/playlist-error-view.component.ts +++ b/libs/portal/shared/ui/src/lib/components/playlist-error-view/playlist-error-view.component.ts @@ -26,10 +26,10 @@ export class PlaylistErrorViewComponent { private readonly currentPlaylist = this.playlistContext.activePlaylist; - readonly description = input(undefined); + readonly description = input(undefined); readonly showIllustration = input(true); readonly showActionButtons = input(true); - readonly title = input(undefined); + readonly title = input(undefined); readonly viewType = input<'ERROR' | 'EMPTY_CATEGORY' | 'NO_SEARCH_RESULTS'>( 'ERROR' ); diff --git a/libs/portal/shared/util/src/lib/navigation/workspace-portal-navigation.ts b/libs/portal/shared/util/src/lib/navigation/workspace-portal-navigation.ts index 8f93b093f..f4b68cd9b 100644 --- a/libs/portal/shared/util/src/lib/navigation/workspace-portal-navigation.ts +++ b/libs/portal/shared/util/src/lib/navigation/workspace-portal-navigation.ts @@ -49,7 +49,7 @@ export function getRecentItemNavigation( if (item.type === 'live') { return buildLiveCollectionNavigationTarget({ mode: 'recent', - sourceType: item.source, + sourceType: normalizeCollectionSourceType(item.source) ?? 'm3u', playlistId: item.playlist_id, itemId: item.xtream_id ?? item.id, title: item.title, @@ -91,7 +91,7 @@ export function getGlobalFavoriteNavigation( if (item.type === 'live') { return buildLiveCollectionNavigationTarget({ mode: 'favorites', - sourceType: item.source, + sourceType: normalizeCollectionSourceType(item.source) ?? 'm3u', playlistId: item.playlist_id, itemId: item.xtream_id ?? item.id, title: item.title, @@ -231,7 +231,7 @@ export function buildOpenCollectionDetailItemState( item: UnifiedCollectionItem ): OpenCollectionDetailItemState { return { - item: normalizeCollectionDetailItem(item), + item: normalizeCollectionDetailItem(item) ?? item, }; } @@ -539,6 +539,14 @@ function normalizeCollectionScope( return value === 'playlist' || value === 'all' ? value : null; } +function normalizeCollectionSourceType( + value: unknown +): CollectionSourceType | null { + return value === 'm3u' || value === 'xtream' || value === 'stalker' + ? value + : null; +} + function buildDashboardCollectionDetailItem( item: PortalFavoriteItem | PortalRecentItem ): UnifiedCollectionItem | null { @@ -669,9 +677,7 @@ function normalizeCollectionDetailItem( } const contentType = toPathSegment(record['contentType']); - const sourceType = toPathSegment( - record['sourceType'] - ) as CollectionSourceType; + const sourceType = normalizeCollectionSourceType(record['sourceType']); const uid = toPathSegment(record['uid']); const name = toPathSegment(record['name']); const playlistId = toPathSegment(record['playlistId']); @@ -680,6 +686,7 @@ function normalizeCollectionDetailItem( !uid || !name || !playlistId || + !sourceType || (contentType !== 'movie' && contentType !== 'series') || (sourceType !== 'xtream' && sourceType !== 'stalker') ) { diff --git a/libs/portal/stalker/data-access/src/lib/stalker-session.service.ts b/libs/portal/stalker/data-access/src/lib/stalker-session.service.ts index 8368691ec..28a703c90 100644 --- a/libs/portal/stalker/data-access/src/lib/stalker-session.service.ts +++ b/libs/portal/stalker/data-access/src/lib/stalker-session.service.ts @@ -8,10 +8,7 @@ import { type StalkerPortalIdentity, } from './stalker-identity.utils'; -export { - getStalkerPortalIdentityFromPlaylist, - normalizeStalkerPortalIdentity, -}; +export { getStalkerPortalIdentityFromPlaylist, normalizeStalkerPortalIdentity }; export type { StalkerPortalIdentity }; /** @@ -100,7 +97,10 @@ export class StalkerSessionService { string, Promise<{ token: string; serialNumber?: string }> >(); - private watchdogIntervals = new Map>(); + private watchdogIntervals = new Map< + string, + ReturnType + >(); private watchdogPlaylists = new Map(); private watchdogInFlight = new Set(); private activeWatchdogPlaylistId: string | null = null; @@ -494,14 +494,16 @@ export class StalkerSessionService { console.error('[StalkerSession] Missing portal URL or MAC address'); throw new Error('Portal URL and MAC address are required'); } + const portalUrl = playlist.portalUrl; + const macAddress = playlist.macAddress; // Create the authentication promise and store it to prevent concurrent auth attempts // Use async/await wrapper to properly clean up on both success and failure const authPromise = (async () => { try { const { token } = await this.authenticate( - playlist.portalUrl, - playlist.macAddress, + portalUrl, + macAddress, identity ); this.setCachedToken(playlist._id, token); diff --git a/libs/portal/stalker/feature/src/lib/stalker-catalog-detail/stalker-catalog-detail.component.ts b/libs/portal/stalker/feature/src/lib/stalker-catalog-detail/stalker-catalog-detail.component.ts index b0bc3d68e..1fcb7b1f8 100644 --- a/libs/portal/stalker/feature/src/lib/stalker-catalog-detail/stalker-catalog-detail.component.ts +++ b/libs/portal/stalker/feature/src/lib/stalker-catalog-detail/stalker-catalog-detail.component.ts @@ -94,9 +94,9 @@ export class StalkerCatalogDetailComponent implements OnDestroy { const item = this.selectedItem(); return Boolean( item && - (this.contentType() === 'series' || - item.is_series === true || - String(item.is_series) === '1') + (this.contentType() === 'series' || + item.is_series === true || + String(item.is_series) === '1') ); }); @@ -257,7 +257,7 @@ export class StalkerCatalogDetailComponent implements OnDestroy { showCopyNotification(): void { this.snackBar.open( this.translateService.instant('PORTALS.STREAM_URL_COPIED'), - null, + undefined, { duration: 2000, } @@ -381,7 +381,7 @@ export class StalkerCatalogDetailComponent implements OnDestroy { 'PORTALS.CONTENT_NOT_AVAILABLE' ) : this.translateService.instant('PORTALS.PLAYBACK_ERROR'); - this.snackBar.open(errorMessage, null, { + this.snackBar.open(errorMessage, undefined, { duration: 3000, }); } diff --git a/libs/portal/stalker/feature/src/lib/stalker-catalog-facade.service.ts b/libs/portal/stalker/feature/src/lib/stalker-catalog-facade.service.ts index ccdadf6e8..9618de29b 100644 --- a/libs/portal/stalker/feature/src/lib/stalker-catalog-facade.service.ts +++ b/libs/portal/stalker/feature/src/lib/stalker-catalog-facade.service.ts @@ -27,8 +27,7 @@ function calculateProgress(position: PlaybackPositionData | undefined): number { return 0; } - const percent = - (position.positionSeconds / position.durationSeconds) * 100; + const percent = (position.positionSeconds / position.durationSeconds) * 100; if (position.positionSeconds > 10 && percent < 1) { return 1; @@ -38,20 +37,17 @@ function calculateProgress(position: PlaybackPositionData | undefined): number { } @Injectable() -export class StalkerCatalogFacadeService - implements - StalkerPortalCatalogFacade< - Record, - StalkerVodSource, - StalkerVodSource - > -{ +export class StalkerCatalogFacadeService implements StalkerPortalCatalogFacade< + Record, + StalkerVodSource, + StalkerVodSource +> { private readonly stalkerStore = inject(StalkerStore); private readonly playbackPositions = inject(PORTAL_PLAYBACK_POSITIONS); private readonly destroyRef = inject(DestroyRef); - private readonly stalkerPositions = signal>( - new Map() - ); + private readonly stalkerPositions = signal< + Map + >(new Map()); private readonly stalkerSeriesPositions = signal< Map >(new Map()); @@ -72,9 +68,9 @@ export class StalkerCatalogFacadeService this.stalkerStore.isPaginatedContentLoading; readonly selectedCategoryTitle = computed(() => { const category = this.selectedCategory(); - const fromCategory = String( - category?.['category_name'] ?? category?.['name'] ?? '' - ); + const fromCategory = category + ? String(category.category_name ?? '') + : ''; if (fromCategory) { return fromCategory; @@ -125,7 +121,10 @@ export class StalkerCatalogFacadeService if (window.electron?.onPlaybackPositionUpdate) { const unsubscribe = window.electron.onPlaybackPositionUpdate( (data: PlaybackPositionData) => { - if (data.playlistId !== this.playlist()?.id) { + if ( + !data.playlistId || + data.playlistId !== this.playlist()?.id + ) { return; } @@ -138,10 +137,7 @@ export class StalkerCatalogFacadeService this.updateVodPlaybackPosition(data); } - if ( - data.contentType === 'episode' && - data.seriesXtreamId - ) { + if (data.contentType === 'episode' && data.seriesXtreamId) { this.updateSeriesPlaybackPosition(data); } } diff --git a/libs/portal/stalker/feature/src/lib/stalker-collection-detail.component.ts b/libs/portal/stalker/feature/src/lib/stalker-collection-detail.component.ts index 785376dac..5f8295d59 100644 --- a/libs/portal/stalker/feature/src/lib/stalker-collection-detail.component.ts +++ b/libs/portal/stalker/feature/src/lib/stalker-collection-detail.component.ts @@ -181,7 +181,7 @@ export class StalkerCollectionDetailComponent { this.originalState.selectedContentType ); this.stalkerStore.setSelectedCategory( - this.originalState.selectedCategoryId + this.originalState.selectedCategoryId ?? null ); this.stalkerStore.setSelectedItem( this.originalState.selectedItem as never diff --git a/libs/portal/stalker/feature/src/lib/stalker-favorites-button/stalker-favorites-button.component.ts b/libs/portal/stalker/feature/src/lib/stalker-favorites-button/stalker-favorites-button.component.ts index 03eb10750..315f93494 100644 --- a/libs/portal/stalker/feature/src/lib/stalker-favorites-button/stalker-favorites-button.component.ts +++ b/libs/portal/stalker/feature/src/lib/stalker-favorites-button/stalker-favorites-button.component.ts @@ -2,7 +2,7 @@ import { AsyncPipe } from '@angular/common'; import { Component, inject, input } from '@angular/core'; import { MatIconModule } from '@angular/material/icon'; import { TranslateModule } from '@ngx-translate/core'; -import { BehaviorSubject, map, switchMap } from 'rxjs'; +import { BehaviorSubject, map, of, switchMap } from 'rxjs'; import { PlaylistsService } from '@iptvnator/services'; import { StalkerStore } from '@iptvnator/portal/stalker/data-access'; import { @@ -33,11 +33,12 @@ export class FavoritesButtonComponent { private readonly favoritesChanged$ = new BehaviorSubject(undefined); readonly isFavorite$ = this.favoritesChanged$.pipe( - switchMap(() => - this.playlistService.getPortalFavorites( - this.stalkerStore.currentPlaylist()?._id - ) - ), + switchMap(() => { + const playlistId = this.stalkerStore.currentPlaylist()?._id; + return playlistId + ? this.playlistService.getPortalFavorites(playlistId) + : of([]); + }), map((favorites) => favorites.some((favorite) => matchesFavoriteById(favorite, this.itemId()) diff --git a/libs/portal/stalker/feature/src/lib/stalker-inline-detail/stalker-inline-detail.component.html b/libs/portal/stalker/feature/src/lib/stalker-inline-detail/stalker-inline-detail.component.html index fa933a512..16cdd2eff 100644 --- a/libs/portal/stalker/feature/src/lib/stalker-inline-detail/stalker-inline-detail.component.html +++ b/libs/portal/stalker/feature/src/lib/stalker-inline-detail/stalker-inline-detail.component.html @@ -8,10 +8,10 @@ } @else if (isSeries()) { - } @else if (vodDetailsItem()) { + } @else if (vodDetailsItem(); as item) { { - favs.forEach((fav: StalkerFavoriteItem) => { - if (fav.id !== undefined) { - this.favorites.set( - normalizeStalkerEntityId(fav.id), - true - ); - } + const playlistId = this.stalkerStore.currentPlaylist()?._id; + if (playlistId) { + this.playlistService + .getPortalFavorites(playlistId) + .pipe(takeUntilDestroyed()) + .subscribe((favs) => { + favs.forEach((fav: StalkerFavoriteItem) => { + if (fav.id !== undefined) { + this.favorites.set( + normalizeStalkerEntityId(fav.id), + true + ); + } + }); }); - }); + } // Reset channels/page on category change effect(() => { @@ -427,10 +436,10 @@ export class StalkerLiveStreamLayoutComponent implements OnDestroy { this.logger.error('Playback failed', error); const errorMessage = - error?.message === 'nothing_to_play' + error instanceof Error && error.message === 'nothing_to_play' ? this.translate.instant('PORTALS.CONTENT_NOT_AVAILABLE') : this.translate.instant('PORTALS.PLAYBACK_ERROR'); - this.snackBar.open(errorMessage, null, { duration: 3000 }); + this.snackBar.open(errorMessage, undefined, { duration: 3000 }); } } @@ -446,9 +455,10 @@ export class StalkerLiveStreamLayoutComponent implements OnDestroy { return this.playbackResolution.promise; } + const playableItem = this.toPlayableChannel(item); const promise = this.isRadioMode() - ? this.stalkerStore.resolveRadioPlayback(item) - : this.stalkerStore.resolveItvPlayback(item); + ? this.stalkerStore.resolveRadioPlayback(playableItem) + : this.stalkerStore.resolveItvPlayback(playableItem); this.playbackResolution = { channelId: playbackChannelId, promise }; const cleanup = () => { @@ -468,8 +478,9 @@ export class StalkerLiveStreamLayoutComponent implements OnDestroy { this.stalkerStore.removeFromFavorites(itemId); this.favorites.delete(itemId); } else { + const playableItem = this.toPlayableChannel(item); this.stalkerStore.addToFavorites({ - ...item, + ...playableItem, category_id: this.isRadioMode() ? 'radio' : 'itv', title: item.o_name || item.name, cover: item.logo, @@ -620,6 +631,8 @@ export class StalkerLiveStreamLayoutComponent implements OnDestroy { const nowMs = Date.now(); if ( + startMs !== null && + stopMs !== null && Number.isFinite(startMs) && Number.isFinite(stopMs) && nowMs >= startMs && @@ -636,6 +649,11 @@ export class StalkerLiveStreamLayoutComponent implements OnDestroy { this.currentProgramsProgress.delete(channelId); } + private toPlayableChannel(item: StalkerItvChannel): StalkerPlayableChannel { + const { is_series, ...rest } = item; + return is_series == null ? rest : { ...rest, is_series }; + } + private setupScrollListener() { this.removeScrollListener(); diff --git a/libs/portal/stalker/feature/src/lib/stalker-search/stalker-search.component.html b/libs/portal/stalker/feature/src/lib/stalker-search/stalker-search.component.html index 985dbfb51..7284f767f 100644 --- a/libs/portal/stalker/feature/src/lib/stalker-search/stalker-search.component.html +++ b/libs/portal/stalker/feature/src/lib/stalker-search/stalker-search.component.html @@ -23,7 +23,9 @@ (inlineTimeUpdated)="handleInlineTimeUpdate($event)" (inlinePlaybackClosed)="closeInlinePlayer()" (streamUrlCopied)="showCopyNotification()" - (inlineExternalFallbackRequested)="handleExternalFallbackRequest($event)" + (inlineExternalFallbackRequested)=" + handleExternalFallbackRequest($event) + " (favoriteToggled)="onVodFavoriteToggled($event)" (backClicked)="onVodBack()" /> @@ -46,7 +48,7 @@
@for (item of searchResultsResource.value(); track item.id) { >({ series: false, vod: true, }); @@ -127,7 +129,7 @@ export class StalkerSearchComponent { return playlist?.macAddress ? playlist : null; }); - readonly selectedFilterType = signal('vod'); + readonly selectedFilterType = signal('vod'); private readonly favoritesRefresh = createRefreshTrigger(); itemDetails: StalkerSelectedVodItem | null = null; @@ -167,6 +169,10 @@ export class StalkerSearchComponent { const playlist = this.currentPlaylist(); if (!playlist) return []; const { portalUrl, macAddress } = playlist; + if (!portalUrl || !macAddress) { + return []; + } + const contentType = params.contentType; let token: string | undefined; let serialNumber: string | undefined; @@ -189,9 +195,9 @@ export class StalkerSearchComponent { url: portalUrl, macAddress, params: { - action: StalkerContentTypes[params.contentType] + action: StalkerContentTypes[contentType] .getContentAction, - type: params.contentType, + type: contentType, search: params.search, max_page_items: 100, }, @@ -199,16 +205,10 @@ export class StalkerSearchComponent { serialNumber, } ); - if (response) { - const items = response.js?.data || []; - return items.map((item: StalkerVodSource) => - this.processItemUrls(item, portalUrl) - ); - } else { - throw new Error( - `Error: ${response.message} (Status: ${response.status})` - ); - } + const items = response.js?.data || []; + return items.map((item: StalkerVodSource) => + this.processItemUrls(item, portalUrl) + ); }, }); @@ -243,23 +243,26 @@ export class StalkerSearchComponent { this.searchTerm.set(term); } - updateFilter(key: string, value: boolean) { + updateFilter(key: StalkerSearchContentType, value: boolean) { if (value) { // Single selection mode - set clicked filter, disable others this.selectedFilterType.set(key); - this.filters.update((f) => { - const newFilters: Record = {}; - Object.keys(f).forEach((k) => { - newFilters[k] = k === key; + this.filters.update(() => { + const newFilters: Record = { + series: false, + vod: false, + }; + this.filterConfig.forEach((filter) => { + newFilters[filter.key] = filter.key === key; }); - return newFilters as typeof f; + return newFilters; }); } } selectItem(item: StalkerVodSource) { this.closeInlinePlayer(); - const hasEmbeddedSeries = item.series?.length > 0; + const hasEmbeddedSeries = (item.series?.length ?? 0) > 0; const needsSeriesFetch = this.selectedFilterType() === 'vod' && !hasEmbeddedSeries && diff --git a/libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-view.component.html b/libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-view.component.html index 7bcb88687..25e2a5d69 100644 --- a/libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-view.component.html +++ b/libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-view.component.html @@ -8,14 +8,10 @@ @if (serial.info.releasedate) { - {{ - serial.info.releasedate - }} + {{ serial.info.releasedate }} } @if (serial.info.genre) { - {{ - serial.info.genre - }} + {{ serial.info.genre }} } @if (serial.info.rating_imdb) { @@ -33,9 +29,7 @@ @if (serial.info.actors) {
- {{ - 'XTREAM.ACTORS' | translate - }} + {{ 'XTREAM.ACTORS' | translate }} {{ serial.info.actors }}
} @@ -75,10 +69,7 @@
} - +
@if (inlinePlayback(); as playback) { @@ -99,19 +90,20 @@ [seasons]="mappedSeasons()" [seriesId]="toSeriesId(serial.id)" [playlistId]="stalkerStore.currentPlaylist()?._id ?? ''" - [seriesTitle]="serial.info?.name" + [seriesTitle]="serial.info.name" [playbackPositions]="episodePlaybackPositions()" [openingEpisodeId]="openingEpisodeId()" [activeEpisodeId]="activeEpisodeId()" [isLoading]=" isVodSeriesSeasonsLoading() || - (seasonContainer?.selectedSeason && - isCurrentSeasonLoading(seasonContainer.selectedSeason)) + isCurrentSeasonLoading(seasonContainer?.selectedSeason) " (seasonSelected)="onSeasonSelected($event)" (episodeClicked)="onEpisodeClicked($event)" (episodeDownloadRequested)="downloadEpisode($event)" - (playbackToggleRequested)="handlePlaybackToggleRequested($event)" + (playbackToggleRequested)=" + handlePlaybackToggleRequested($event) + " #seasonContainer /> } diff --git a/libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-view.component.ts b/libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-view.component.ts index 1df5faad2..2b6a8276f 100644 --- a/libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-view.component.ts +++ b/libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-view.component.ts @@ -334,7 +334,8 @@ export class StalkerSeriesViewComponent implements OnDestroy { /** * Determines if the current selected season is loading */ - isCurrentSeasonLoading(seasonKey: string): boolean { + isCurrentSeasonLoading(seasonKey?: string): boolean { + if (!seasonKey) return false; if (!this.isVodSeries()) return false; const season = this.vodSeriesSeasons().find( (s) => getVodSeriesSeasonKey(s) === seasonKey @@ -498,7 +499,7 @@ export class StalkerSeriesViewComponent implements OnDestroy { showCopyNotification(): void { this.snackBar.open( this.translateService.instant('PORTALS.STREAM_URL_COPIED'), - null, + undefined, { duration: 2000, } @@ -546,7 +547,7 @@ export class StalkerSeriesViewComponent implements OnDestroy { 'PORTALS.CONTENT_NOT_AVAILABLE' ) : this.translateService.instant('PORTALS.PLAYBACK_ERROR'); - this.snackBar.open(errorMessage, null, { + this.snackBar.open(errorMessage, undefined, { duration: 3000, }); } diff --git a/libs/portal/stalker/feature/src/lib/stalker-vod-playback-controller.ts b/libs/portal/stalker/feature/src/lib/stalker-vod-playback-controller.ts index 27bc985f3..ed1f5df40 100644 --- a/libs/portal/stalker/feature/src/lib/stalker-vod-playback-controller.ts +++ b/libs/portal/stalker/feature/src/lib/stalker-vod-playback-controller.ts @@ -56,7 +56,7 @@ export class StalkerVodPlaybackController { : this.config.translateService.instant( 'PORTALS.PLAYBACK_ERROR' ); - this.config.snackBar.open(errorMessage, null, { + this.config.snackBar.open(errorMessage, undefined, { duration: 3000, }); } @@ -122,7 +122,7 @@ export class StalkerVodPlaybackController { showCopyNotification(): void { this.config.snackBar.open( this.config.translateService.instant('PORTALS.STREAM_URL_COPIED'), - null, + undefined, { duration: 2000, } diff --git a/libs/portal/xtream/data-access/src/lib/data-sources/electron-xtream-data-source.ts b/libs/portal/xtream/data-access/src/lib/data-sources/electron-xtream-data-source.ts index e2eabcedc..a2365a76b 100644 --- a/libs/portal/xtream/data-access/src/lib/data-sources/electron-xtream-data-source.ts +++ b/libs/portal/xtream/data-access/src/lib/data-sources/electron-xtream-data-source.ts @@ -360,7 +360,7 @@ export class ElectronXtreamDataSource implements IXtreamDataSource { ): Promise { return this.dbService.saveXtreamContent( playlistId, - streams, + streams as Parameters[1], type, onProgress, options diff --git a/libs/portal/xtream/data-access/src/lib/data-sources/pwa-xtream-data-source.ts b/libs/portal/xtream/data-access/src/lib/data-sources/pwa-xtream-data-source.ts index 4c07c657c..0d0e5606b 100644 --- a/libs/portal/xtream/data-access/src/lib/data-sources/pwa-xtream-data-source.ts +++ b/libs/portal/xtream/data-access/src/lib/data-sources/pwa-xtream-data-source.ts @@ -616,8 +616,8 @@ export class PwaXtreamDataSource implements IXtreamDataSource { // Sort by updatedAt descending playlistPositions.sort( (a, b) => - new Date(b.updatedAt).getTime() - - new Date(a.updatedAt).getTime() + new Date(b.updatedAt ?? '').getTime() - + new Date(a.updatedAt ?? '').getTime() ); return limit ? playlistPositions.slice(0, limit) : playlistPositions; diff --git a/libs/portal/xtream/data-access/src/lib/stores/features/with-player.feature.ts b/libs/portal/xtream/data-access/src/lib/stores/features/with-player.feature.ts index 27fd022b5..199dd2e7d 100644 --- a/libs/portal/xtream/data-access/src/lib/stores/features/with-player.feature.ts +++ b/libs/portal/xtream/data-access/src/lib/stores/features/with-player.feature.ts @@ -6,7 +6,10 @@ import { withState, } from '@ngrx/signals'; import { PORTAL_PLAYER } from '@iptvnator/portal/shared/util'; -import { XtreamSerieEpisode, XtreamVodDetails } from '@iptvnator/shared/interfaces'; +import { + XtreamSerieEpisode, + XtreamVodDetails, +} from '@iptvnator/shared/interfaces'; import { XtreamCredentials } from '../../services/xtream-api.service'; import { XtreamUrlService } from '../../services/xtream-url.service'; @@ -80,10 +83,11 @@ export function withPlayer() { /** * Helper to get playlist with headers from parent store */ - const getPlaylistFromStore = (): XtreamPlaylistHeadersLike | null => { - const storeAny = store as ParentPlayerStoreLike; - return storeAny.currentPlaylist?.(); - }; + const getPlaylistFromStore = + (): XtreamPlaylistHeadersLike | null => { + const storeAny = store as ParentPlayerStoreLike; + return storeAny.currentPlaylist?.() ?? null; + }; return { /** diff --git a/libs/portal/xtream/data-access/src/lib/with-recent-items.ts b/libs/portal/xtream/data-access/src/lib/with-recent-items.ts index 8d7fda351..ba60a3204 100644 --- a/libs/portal/xtream/data-access/src/lib/with-recent-items.ts +++ b/libs/portal/xtream/data-access/src/lib/with-recent-items.ts @@ -51,7 +51,7 @@ function mapDbRecentItem( export const withRecentItems = function () { const logger = createLogger('withRecentItems'); return signalStoreFeature( - withState({ + withState<{ recentItems: RecentlyViewedItem[] }>({ recentItems: [], }), withMethods((store, dataSource = inject(XTREAM_DATA_SOURCE)) => ({ @@ -81,7 +81,7 @@ export const withRecentItems = function () { addRecentItem: rxMethod<{ xtreamId: number | string; contentType: 'live' | 'movie' | 'series'; - playlist: Signal<{ id: string }>; + playlist: Signal<{ id: string } | null | undefined>; backdropUrl?: string; }>( pipe( @@ -92,7 +92,7 @@ export const withRecentItems = function () { playlist, backdropUrl, }) => { - const playlistId = playlist().id; + const playlistId = playlist()?.id; const normalizedXtreamId = Number(xtreamId); if ( !playlistId || @@ -145,10 +145,10 @@ export const withRecentItems = function () { }: { xtreamId: number | string; contentType: 'live' | 'movie' | 'series'; - playlist: Signal<{ id: string }>; + playlist: Signal<{ id: string } | null | undefined>; backdropUrl?: string; }): Promise { - const playlistId = playlist().id; + const playlistId = playlist()?.id; const normalizedXtreamId = Number(xtreamId); const normalizedBackdropUrl = backdropUrl?.trim(); if ( diff --git a/libs/portal/xtream/feature/src/lib/portal-channels-list/portal-channels-list.component.html b/libs/portal/xtream/feature/src/lib/portal-channels-list/portal-channels-list.component.html index f25a16c8b..13974df87 100644 --- a/libs/portal/xtream/feature/src/lib/portal-channels-list/portal-channels-list.component.html +++ b/libs/portal/xtream/feature/src/lib/portal-channels-list/portal-channels-list.component.html @@ -26,7 +26,7 @@ trackBy: trackBy; templateCacheSize: 0 " - [name]="item.title || item.name" + [name]="item.title || item.name || ''" [logo]="item.stream_icon ?? item.poster_url" [selected]=" xtreamStore.selectedItem()?.xtream_id === item.xtream_id diff --git a/libs/portal/xtream/feature/src/lib/recently-added/recently-added.component.html b/libs/portal/xtream/feature/src/lib/recently-added/recently-added.component.html index de551d98f..ef5334448 100644 --- a/libs/portal/xtream/feature/src/lib/recently-added/recently-added.component.html +++ b/libs/portal/xtream/feature/src/lib/recently-added/recently-added.component.html @@ -32,7 +32,7 @@ > @for (item of recentlyAddedVod(); track item.xtream_id) { @for (item of recentlyAddedSeries(); track item.xtream_id) { @for (item of recentlyAddedLive(); track item.xtream_id) { { - return items.reduce>( - (groups, item) => { - const key = String(item.playlist_name); - if (!groups[key]) { - groups[key] = []; - } - groups[key].push(item); - return groups; - }, - {} - ); + return items.reduce>((groups, item) => { + const key = String(item.playlist_name); + if (!groups[key]) { + groups[key] = []; + } + groups[key].push(item); + return groups; + }, {}); } @Component({ @@ -199,7 +196,7 @@ export class SearchResultsComponent implements AfterViewInit { } ngAfterViewInit() { - this.xtreamStore.setSelectedContentType(undefined); + this.xtreamStore.setSelectedContentType('vod'); setTimeout(() => { this.searchLayoutComponent()?.focusSearchInput(); }); diff --git a/libs/portal/xtream/feature/src/lib/serial-details/serial-details.component.html b/libs/portal/xtream/feature/src/lib/serial-details/serial-details.component.html index cd8558b1f..ad0ab5e9b 100644 --- a/libs/portal/xtream/feature/src/lib/serial-details/serial-details.component.html +++ b/libs/portal/xtream/feature/src/lib/serial-details/serial-details.component.html @@ -1,46 +1,47 @@ @let item = selectedItem(); -@if (item?.info) { +@if (item && item.info) { + @let info = item.info; - @if (item.info?.releaseDate) { + @if (info.releaseDate) { {{ - item.info.releaseDate | slice: 0 : 4 + info.releaseDate | slice: 0 : 4 }} } - @if (item.info?.genre) { - {{ item.info.genre }} + @if (info.genre) { + {{ info.genre }} } - @if (item.info?.episode_run_time) { + @if (info.episode_run_time) { {{ item.info.episode_run_time }} min/ep{{ info.episode_run_time }} min/ep } - @if (item.info?.rating) { + @if (info.rating) { - ⭐ {{ item.info.rating }} + ⭐ {{ info.rating }} } - @if (item.info?.cast) { + @if (info.cast) {
{{ 'XTREAM.CAST' | translate }} - {{ item.info.cast }} + {{ info.cast }}
} - @if (item.info?.director) { + @if (info.director) {
{{ 'XTREAM.DIRECTOR' | translate }} - {{ item.info.director }} + {{ info.director }}
}
@@ -100,7 +101,7 @@ [seasons]="item.episodes" [seriesId]="item.series_id" [playlistId]="currentPlaylistId()" - [seriesTitle]="item.info?.name" + [seriesTitle]="info.name ?? ''" [playbackPositions]="episodePlaybackPositions()" [xtreamDownloadContext]="xtreamDownloadContext()" [openingEpisodeId]="openingEpisodeId()" @@ -111,7 +112,7 @@
} @else if (detailsError()) { } @else { diff --git a/libs/portal/xtream/feature/src/lib/serial-details/serial-details.component.ts b/libs/portal/xtream/feature/src/lib/serial-details/serial-details.component.ts index d67e95fdc..e7890f92c 100644 --- a/libs/portal/xtream/feature/src/lib/serial-details/serial-details.component.ts +++ b/libs/portal/xtream/feature/src/lib/serial-details/serial-details.component.ts @@ -154,7 +154,12 @@ export class SerialDetailsComponent implements OnInit, OnDestroy { const xtreamId = Number(selectedItem?.series_id ?? 0); const backdropUrl = selectedItem?.info?.backdrop_path?.[0]?.trim(); - if (!playlistId || !Number.isFinite(xtreamId) || xtreamId <= 0 || !backdropUrl) { + if ( + !playlistId || + !Number.isFinite(xtreamId) || + xtreamId <= 0 || + !backdropUrl + ) { return; } @@ -246,14 +251,20 @@ export class SerialDetailsComponent implements OnInit, OnDestroy { } playEpisode(episode: XtreamSerieEpisode): void { + const playlist = this.xtreamStore.currentPlaylist(); + const selectedItem = this.selectedItem(); + if (!playlist || !selectedItem) { + return; + } + this.addToRecentlyViewed(this.route.snapshot.params.serialId); const streamUrl = this.xtreamStore.constructEpisodeStreamUrl(episode); const contentInfo: PlayerContentInfo = { - playlistId: this.xtreamStore.currentPlaylist().id, + playlistId: playlist.id, contentXtreamId: Number(episode.id), contentType: 'episode', - seriesXtreamId: Number(this.selectedItem().series_id), + seriesXtreamId: Number(selectedItem.series_id), seasonNumber: Number(episode.season), episodeNumber: Number(episode.episode_num), }; @@ -265,7 +276,7 @@ export class SerialDetailsComponent implements OnInit, OnDestroy { const playback: ResolvedPortalPlayback = { streamUrl, title: episode.title, - thumbnail: this.selectedItem().info.cover, + thumbnail: selectedItem.info.cover, startTime: position?.positionSeconds, contentInfo, }; @@ -283,9 +294,14 @@ export class SerialDetailsComponent implements OnInit, OnDestroy { } toggleFavorite(): void { + const playlist = this.xtreamStore.currentPlaylist(); + if (!playlist) { + return; + } + this.xtreamStore.toggleFavorite( this.route.snapshot.params.serialId, - this.xtreamStore.currentPlaylist().id, + playlist.id, 'series', this.selectedItem()?.info?.backdrop_path?.[0] ); @@ -327,7 +343,7 @@ export class SerialDetailsComponent implements OnInit, OnDestroy { showCopyNotification(): void { this.snackBar.open( this.translateService.instant('PORTALS.STREAM_URL_COPIED'), - null, + undefined, { duration: 2000, } diff --git a/libs/portal/xtream/feature/src/lib/vod-details/vod-details-route.component.html b/libs/portal/xtream/feature/src/lib/vod-details/vod-details-route.component.html index 9a8d1c12b..b13bbd318 100644 --- a/libs/portal/xtream/feature/src/lib/vod-details/vod-details-route.component.html +++ b/libs/portal/xtream/feature/src/lib/vod-details/vod-details-route.component.html @@ -60,8 +60,12 @@ class="play-btn play-btn--resume" (click)="onPrimaryAction(item)" [disabled]="isExternalLaunchPending()" - [class.play-btn--state-launching]="externalPrimaryButtonState() === 'launching'" - [class.play-btn--state-stop]="externalPrimaryButtonState() === 'stop'" + [class.play-btn--state-launching]=" + externalPrimaryButtonState() === 'launching' + " + [class.play-btn--state-stop]=" + externalPrimaryButtonState() === 'stop' + " > } @else if (detailsError()) { } @else { diff --git a/libs/portal/xtream/feature/src/lib/vod-details/vod-details-route.component.ts b/libs/portal/xtream/feature/src/lib/vod-details/vod-details-route.component.ts index b094498e7..27c728178 100644 --- a/libs/portal/xtream/feature/src/lib/vod-details/vod-details-route.component.ts +++ b/libs/portal/xtream/feature/src/lib/vod-details/vod-details-route.component.ts @@ -98,21 +98,28 @@ export class VodDetailsRouteComponent implements OnInit, OnDestroy { .find( (category) => String( - (category as XtreamCategory & { id?: string | number }) - .category_id ?? - (category as XtreamCategory & { id?: string | number }) - .id + ( + category as XtreamCategory & { + id?: string | number; + } + ).category_id ?? + ( + category as XtreamCategory & { + id?: string | number; + } + ).id ) === String(categoryId) ) ?? null ); }); readonly selectedCatalogItem = computed< - (Partial & { - id?: string | number; - poster_url?: string; - title?: string; - xtream_id?: string | number; - }) | null + | (Partial & { + id?: string | number; + poster_url?: string; + title?: string; + xtream_id?: string | number; + }) + | null >(() => { const vodId = this.selectedVodId(); if (!Number.isFinite(vodId) || vodId <= 0) { @@ -120,29 +127,27 @@ export class VodDetailsRouteComponent implements OnInit, OnDestroy { } return ( - this.xtreamStore - .vodStreams() - .find( - (item) => - Number( + this.xtreamStore.vodStreams().find( + (item) => + Number( + ( + item as XtreamVodStream & { + id?: string | number; + xtream_id?: string | number; + } + ).xtream_id ?? ( item as XtreamVodStream & { id?: string | number; - xtream_id?: string | number; } - ).xtream_id ?? - ( - item as XtreamVodStream & { - id?: string | number; - } - ).stream_id ?? - ( - item as XtreamVodStream & { - id?: string | number; - } - ).id - ) === vodId - ) ?? null + ).stream_id ?? + ( + item as XtreamVodStream & { + id?: string | number; + } + ).id + ) === vodId + ) ?? null ); }); readonly selectedVodInfo = computed(() => { @@ -278,9 +283,15 @@ export class VodDetailsRouteComponent implements OnInit, OnDestroy { effect(() => { const playlistId = this.xtreamStore.currentPlaylist()?.id; const vodId = Number(this.route.snapshot.params.vodId); - const backdropUrl = this.selectedVodInfo()?.backdrop_path?.[0]?.trim(); + const backdropUrl = + this.selectedVodInfo()?.backdrop_path?.[0]?.trim(); - if (!playlistId || !Number.isFinite(vodId) || vodId <= 0 || !backdropUrl) { + if ( + !playlistId || + !Number.isFinite(vodId) || + vodId <= 0 || + !backdropUrl + ) { return; } @@ -302,7 +313,8 @@ export class VodDetailsRouteComponent implements OnInit, OnDestroy { this.unsubscribePositionUpdates = window.electron.onPlaybackPositionUpdate( (data: PlaybackPositionData) => { - const playlistId = this.xtreamStore.currentPlaylist()?.id; + const playlistId = + this.xtreamStore.currentPlaylist()?.id; const vodId = Number(this.route.snapshot.params.vodId); if ( @@ -325,7 +337,10 @@ export class VodDetailsRouteComponent implements OnInit, OnDestroy { this.logger.warn('Deferring VOD details init: playlist not ready'); return; } - this.initializeVodDetails(currentPlaylist.id, Number(this.route.snapshot.params.vodId)); + this.initializeVodDetails( + currentPlaylist.id, + Number(this.route.snapshot.params.vodId) + ); this.detailsInitDone.set(true); } @@ -335,7 +350,16 @@ export class VodDetailsRouteComponent implements OnInit, OnDestroy { this.xtreamStore.setSelectedItem(null); } - playVod(vodItem: XtreamVodDetails): void { + playVod(vodItem: XtreamVodDetails | null): void { + if (!vodItem) { + return; + } + + const playlist = this.xtreamStore.currentPlaylist(); + if (!playlist) { + return; + } + const info = getXtreamVodInfo(vodItem); this.addToRecentlyViewed(); const streamUrl = this.xtreamStore.constructVodStreamUrl(vodItem); @@ -350,13 +374,13 @@ export class VodDetailsRouteComponent implements OnInit, OnDestroy { this.logger.debug('playVod resolved ID', { id, vodItem }); const contentInfo: PlayerContentInfo = { - playlistId: this.xtreamStore.currentPlaylist().id, + playlistId: playlist.id, contentXtreamId: id, contentType: 'vod', }; const playback: ResolvedPortalPlayback = { streamUrl, - title: info?.name ?? vodItem.movie_data?.name, + title: info?.name ?? vodItem.movie_data?.name ?? 'Unknown', thumbnail: info?.movie_image, contentInfo, }; @@ -364,7 +388,16 @@ export class VodDetailsRouteComponent implements OnInit, OnDestroy { this.startPlayback(playback); } - resumeVod(vodItem: XtreamVodDetails): void { + resumeVod(vodItem: XtreamVodDetails | null): void { + if (!vodItem) { + return; + } + + const playlist = this.xtreamStore.currentPlaylist(); + if (!playlist) { + return; + } + const info = getXtreamVodInfo(vodItem); this.addToRecentlyViewed(); const vodId = Number(this.route.snapshot.params.vodId); @@ -372,13 +405,13 @@ export class VodDetailsRouteComponent implements OnInit, OnDestroy { const streamUrl = this.xtreamStore.constructVodStreamUrl(vodItem); const contentInfo: PlayerContentInfo = { - playlistId: this.xtreamStore.currentPlaylist().id, + playlistId: playlist.id, contentXtreamId: vodId, contentType: 'vod', }; const playback: ResolvedPortalPlayback = { streamUrl, - title: info?.name ?? vodItem.movie_data?.name, + title: info?.name ?? vodItem.movie_data?.name ?? 'Unknown', thumbnail: info?.movie_image, startTime: position?.positionSeconds, contentInfo, @@ -387,7 +420,11 @@ export class VodDetailsRouteComponent implements OnInit, OnDestroy { this.startPlayback(playback); } - onPrimaryAction(vodItem: XtreamVodDetails): void { + onPrimaryAction(vodItem: XtreamVodDetails | null): void { + if (!vodItem) { + return; + } + if (this.isExternalStopAction()) { void this.stopExternalPlayback(); return; @@ -402,7 +439,9 @@ export class VodDetailsRouteComponent implements OnInit, OnDestroy { } async stopExternalPlayback(): Promise { - await this.externalPlayback.closeSession(this.matchedExternalPlayback()); + await this.externalPlayback.closeSession( + this.matchedExternalPlayback() + ); } formatPosition(): string { @@ -412,15 +451,18 @@ export class VodDetailsRouteComponent implements OnInit, OnDestroy { const date = new Date(0); date.setSeconds(position.positionSeconds); const timeString = date.toISOString().substr(11, 8); - return timeString.startsWith('00:') - ? timeString.substr(3) - : timeString; + return timeString.startsWith('00:') ? timeString.substr(3) : timeString; } toggleFavorite(): void { + const playlist = this.xtreamStore.currentPlaylist(); + if (!playlist) { + return; + } + this.xtreamStore.toggleFavorite( this.route.snapshot.params.vodId, - this.xtreamStore.currentPlaylist().id, + playlist.id, 'movie', this.selectedVodInfo()?.backdrop_path?.[0] ); @@ -462,7 +504,7 @@ export class VodDetailsRouteComponent implements OnInit, OnDestroy { showCopyNotification(): void { this.snackBar.open( this.translateService.instant('PORTALS.STREAM_URL_COPIED'), - null, + undefined, { duration: 2000, } @@ -476,7 +518,11 @@ export class VodDetailsRouteComponent implements OnInit, OnDestroy { ); } - async downloadVod(vodItem: XtreamVodDetails): Promise { + async downloadVod(vodItem: XtreamVodDetails | null): Promise { + if (!vodItem) { + return; + } + const info = getXtreamVodInfo(vodItem); const streamUrl = this.xtreamStore.constructVodStreamUrl(vodItem); const routeVodId = this.route.snapshot.params.vodId; @@ -488,6 +534,9 @@ export class VodDetailsRouteComponent implements OnInit, OnDestroy { ); const playlist = this.xtreamStore.currentPlaylist(); + if (!playlist) { + return; + } await this.downloadsService.startDownload({ playlistId: playlist.id, diff --git a/libs/portal/xtream/feature/src/lib/xtream-collection-detail.component.ts b/libs/portal/xtream/feature/src/lib/xtream-collection-detail.component.ts index 815053f34..737acfa7c 100644 --- a/libs/portal/xtream/feature/src/lib/xtream-collection-detail.component.ts +++ b/libs/portal/xtream/feature/src/lib/xtream-collection-detail.component.ts @@ -41,7 +41,7 @@ interface XtreamCollectionStateSnapshot { } @else { diff --git a/libs/services/src/lib/data.service.ts b/libs/services/src/lib/data.service.ts index ed23adffc..a01e6f383 100644 --- a/libs/services/src/lib/data.service.ts +++ b/libs/services/src/lib/data.service.ts @@ -4,12 +4,36 @@ type ElectronProcessWindow = Window & { }; }; +export interface ElectronRemoteProcess { + argv: string[]; + platform: + | 'aix' + | 'android' + | 'darwin' + | 'freebsd' + | 'haiku' + | 'linux' + | 'openbsd' + | 'sunos' + | 'win32' + | 'cygwin' + | 'netbsd' + | string; +} + +export interface ElectronRemote { + app?: { + getVersion: () => string; + }; + process: ElectronRemoteProcess; +} + export abstract class DataService { get isElectron(): boolean { const currentWindow = window as ElectronProcessWindow; return Boolean(currentWindow.process?.type); } - get remote() { + get remote(): ElectronRemote | null { return null; } get ipcRenderer() { diff --git a/libs/ui/components/src/lib/channel-list-container/all-channels-view/all-channels-view.component.html b/libs/ui/components/src/lib/channel-list-container/all-channels-view/all-channels-view.component.html index 490185458..92006c4d0 100644 --- a/libs/ui/components/src/lib/channel-list-container/all-channels-view/all-channels-view.component.html +++ b/libs/ui/components/src/lib/channel-list-container/all-channels-view/all-channels-view.component.html @@ -93,7 +93,7 @@ (contextMenuRequested)="onChannelContextMenu(channel, $event)" [selected]="activeChannelUrl() === channel?.url" [showFavoriteButton]="true" - [isFavorite]="favoriteIds().has(channel?.url)" + [isFavorite]="favoriteIds().has(channel?.url ?? '')" (favoriteToggled)="onFavoriteToggle(channel, $event)" /> 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 e93d8f2a3..855ebd5d3 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 @@ -74,7 +74,10 @@ }
} @else if (showSeasonEmptyState()) { -
+
@@ -109,7 +112,7 @@
@if (episodeInfo?.movie_image) { - @if (getEpisodePositionText(episode); as time) { -
+ @if ( + getEpisodePositionText(episode); + as time + ) { +
{{ time }}
} @@ -141,17 +149,30 @@ - } @else if (isEpisodeDownloading(episode)) { + } @else if ( + isEpisodeDownloading(episode) + ) { @@ -159,8 +180,16 @@ @@ -173,7 +202,8 @@ [matTooltip]=" (isEpisodeWatched(episode) ? 'XTREAM.MARK_UNWATCHED' - : 'XTREAM.MARK_WATCHED') | translate + : 'XTREAM.MARK_WATCHED' + ) | translate " > {{ @@ -186,7 +216,9 @@
@if (isEpisodeLaunching(episode)) { - + } @else { {{ isEpisodeActiveExternal(episode) @@ -202,16 +234,17 @@

- {{ episode.episode_num }}. {{ episode.title }} + {{ episode.episode_num }}. + {{ episode.title }}

@if (episodeInfo?.plot) {

- {{ episodeInfo.plot }} + {{ episodeInfo?.plot }}

} @if (episodeInfo?.duration) { - {{ episodeInfo.duration }} + {{ episodeInfo?.duration }} }
@@ -226,10 +259,18 @@
{{ episode.episode_num }} @@ -239,17 +280,26 @@

{{ episode.title }} @if (isEpisodeWatched(episode)) { - check_circle + check_circle }

@if (episodeInfo?.duration) { - - {{ episodeInfo.duration }} + + {{ episodeInfo?.duration }} } - @if (getEpisodePositionText(episode); as time) { - + @if ( + getEpisodePositionText(episode); + as time + ) { + {{ time }} } @@ -257,14 +307,18 @@
@if (episodeInfo?.plot) {

- {{ episodeInfo.plot }} + {{ episodeInfo?.plot }}

} @if (getEpisodeProgress(episode) > 0) { -
+
} @@ -273,12 +327,17 @@ @@ -302,7 +366,10 @@ mat-icon-button class="action-btn" disabled - [matTooltip]="'DOWNLOADS.STATUS.DOWNLOADING' | translate" + [matTooltip]=" + 'DOWNLOADS.STATUS.DOWNLOADING' + | translate + " > downloading @@ -310,8 +377,12 @@ @@ -324,9 +395,12 @@ [matTooltip]=" (isEpisodeWatched(episode) ? 'XTREAM.MARK_UNWATCHED' - : 'XTREAM.MARK_WATCHED') | translate + : 'XTREAM.MARK_WATCHED' + ) | translate + " + [class.action-btn--success]=" + isEpisodeWatched(episode) " - [class.action-btn--success]="isEpisodeWatched(episode)" > {{ isEpisodeWatched(episode) diff --git a/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-session-controller.ts b/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-session-controller.ts index 22caea71d..f7c21358f 100644 --- a/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-session-controller.ts +++ b/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-session-controller.ts @@ -51,7 +51,7 @@ export class EmbeddedMpvSessionController { this.session.set(session); }); - if (window.electron?.getEmbeddedMpvSupport) { + if (typeof window.electron?.getEmbeddedMpvSupport === 'function') { void this.loadSupport(); } else { this.support.set({ diff --git a/libs/ui/playback/src/lib/embedded-mpv-player/index.ts b/libs/ui/playback/src/lib/embedded-mpv-player/index.ts new file mode 100644 index 000000000..da8fbfccf --- /dev/null +++ b/libs/ui/playback/src/lib/embedded-mpv-player/index.ts @@ -0,0 +1,3 @@ +export * from './embedded-mpv-overlay-visibility.service'; +export * from './embedded-mpv-player.component'; +export * from './embedded-mpv-session-controller'; diff --git a/libs/ui/playback/src/lib/vjs-player/vjs-player.component.ts b/libs/ui/playback/src/lib/vjs-player/vjs-player.component.ts index cc3e2a5c1..e5cacbb7b 100644 --- a/libs/ui/playback/src/lib/vjs-player/vjs-player.component.ts +++ b/libs/ui/playback/src/lib/vjs-player/vjs-player.component.ts @@ -174,7 +174,7 @@ export class VjsPlayerComponent implements OnInit, OnChanges, OnDestroy { audioTracks.addEventListener('addtrack', () => { debugVjsPlayer( '[AudioTrack] addtrack event fired, total tracks:', - this.player.audioTracks().length + audioTracks.length ); this.logAudioTracks(); this.setupAudioTrackMenu(); @@ -182,7 +182,7 @@ export class VjsPlayerComponent implements OnInit, OnChanges, OnDestroy { audioTracks.addEventListener('removetrack', () => { debugVjsPlayer( '[AudioTrack] removetrack event fired, total tracks:', - this.player.audioTracks().length + audioTracks.length ); this.logAudioTracks(); this.setupAudioTrackMenu(); @@ -193,19 +193,19 @@ export class VjsPlayerComponent implements OnInit, OnChanges, OnDestroy { } this.player.on('volumechange', () => { - const currentVolume = this.player.volume(); + const currentVolume = this.player.volume() ?? this.volume(); localStorage.setItem('volume', currentVolume.toString()); }); this.player.on('timeupdate', () => { this.timeUpdate.emit({ - currentTime: this.player.currentTime(), - duration: this.player.duration(), + currentTime: this.player.currentTime() ?? 0, + duration: this.player.duration() ?? 0, }); }); // Attach mpegts.js after Video.js is ready - if (isMpegTs) { + if (isMpegTs && source) { this.initMpegTs(source.src); } }) as unknown as VideoJsPlayer; @@ -311,7 +311,7 @@ export class VjsPlayerComponent implements OnInit, OnChanges, OnDestroy { private initMpegTs(url: string): void { const videoEl = this.player .tech({ IWillNotUseThisInPlugins: true }) - ?.el(); + ?.el?.(); if (!videoEl) return; debugVjsPlayer('Using mpegts.js for TS stream:', url); @@ -473,7 +473,11 @@ export class VjsPlayerComponent implements OnInit, OnChanges, OnDestroy { '[AudioTrack] Audio tracks count:', audioTracks?.length ?? 0 ); - for (let i = 0; i < (audioTracks?.length ?? 0); i++) { + if (!audioTracks) { + return; + } + + for (let i = 0; i < audioTracks.length; i++) { const t = audioTracks[i]; debugVjsPlayer( `[AudioTrack] Track ${i}: label="${t.label}", language="${t.language}", enabled=${t.enabled}, kind="${t.kind}"` @@ -529,9 +533,9 @@ export class VjsPlayerComponent implements OnInit, OnChanges, OnDestroy { } let audioButton = - controlBar.getChild('audioTrackButton') ?? - controlBar.getChild('AudioTrackButton'); - if (!audioButton) { + controlBar.getChild?.('audioTrackButton') ?? + controlBar.getChild?.('AudioTrackButton'); + if (!audioButton && controlBar.addChild) { audioButton = controlBar.addChild('audioTrackButton', {}); } diff --git a/libs/workspace/shell/feature/src/lib/workspace-context-panel/components/workspace-context-category-view.component.html b/libs/workspace/shell/feature/src/lib/workspace-context-panel/components/workspace-context-category-view.component.html index bb70806f8..1082a6fb7 100644 --- a/libs/workspace/shell/feature/src/lib/workspace-context-panel/components/workspace-context-category-view.component.html +++ b/libs/workspace/shell/feature/src/lib/workspace-context-panel/components/workspace-context-category-view.component.html @@ -1,4 +1,4 @@ -@if (items()?.length > 0) { +@if ((items()?.length ?? 0) > 0) {