refactor: add lifecycle-safe router unsubscribe and optimize portal checks

This commit is contained in:
4gray committed 2026-01-10 16:53:05 +01:00
1 parent 6b94fcbdd6
commit 971ad70893
5 files changed
+151 -92

No files matched your search

@@ -1,8 +1,9 @@
import { Component, effect, inject, OnDestroy } from '@angular/core';
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
import { ActivatedRoute, Router, RouterOutlet } from '@angular/router';
import { Store } from '@ngrx/store';
import { PlaylistActions } from 'm3u-state';
import { selectPlaylistById } from 'm3u-state';
import { PlaylistActions, selectPlaylistById } from 'm3u-state';
import { map, switchMap } from 'rxjs';
import { NavigationComponent } from '../../xtream-tauri/navigation/navigation.component';
import { NavigationItem } from '../../xtream-tauri/navigation/navigation.interface';
import { StalkerStore } from '../stalker.store';
@@ -38,24 +39,34 @@ export class StalkerShellComponent implements OnDestroy {
},
];
readonly currentPlaylist = this.store.selectSignal(
selectPlaylistById(this.route.snapshot.params.id)
/** Current playlist derived from route params */
readonly currentPlaylist = toSignal(
this.route.params.pipe(
map((params) => params['id']),
switchMap((id) => this.store.select(selectPlaylistById(id)))
)
);
constructor() {
this.store.dispatch(
PlaylistActions.setCurrentPlaylistId({
playlistId: this.route.snapshot.params['id'],
})
);
// Subscribe to route params to handle switching between playlists
this.route.params.pipe(takeUntilDestroyed()).subscribe((params) => {
const playlistId = params['id'];
const childRoute = this.route.snapshot.firstChild;
const path = childRoute?.url[0]?.path;
const validTypes = ['vod', 'series', 'itv'];
const initialType = validTypes.includes(path) ? (path as any) : 'vod';
this.store.dispatch(
PlaylistActions.setCurrentPlaylistId({ playlistId })
);
// Reset store state when switching playlists
this.stalkerStore.resetCategories();
this.stalkerStore.setSelectedCategory(null);
const childRoute = this.route.snapshot.firstChild;
const path = childRoute?.url[0]?.path;
const validTypes = ['vod', 'series', 'itv'];
const initialType = validTypes.includes(path) ? (path as any) : 'vod';
this.stalkerStore.setSelectedContentType(initialType);
});
this.stalkerStore.setSelectedContentType(initialType);
this.stalkerStore.setSelectedCategory(null);
effect(() => {
this.stalkerStore.setCurrentPlaylist(this.currentPlaylist());
});
@@ -1,4 +1,5 @@
import { Component, effect, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ActivatedRoute, Router, RouterOutlet } from '@angular/router';
import { Store } from '@ngrx/store';
import { TranslateModule } from '@ngx-translate/core';
@@ -48,7 +49,33 @@ export class XtreamShellComponent {
},
];
private currentPlaylistId: string | null = null;
private isFirstLoad = true;
constructor() {
// Subscribe to route params to handle switching between playlists
this.route.params.pipe(takeUntilDestroyed()).subscribe((params) => {
const newPlaylistId = params['id'];
if (this.isFirstLoad) {
// First load - just set the playlist ID without resetting
this.xtreamStore.setPlaylistId(newPlaylistId);
this.isFirstLoad = false;
} else if (this.currentPlaylistId !== newPlaylistId) {
// Switching to a different playlist - reset and set new ID
this.xtreamStore.resetStore(newPlaylistId);
}
this.currentPlaylistId = newPlaylistId;
this.store.dispatch(
PlaylistActions.setActivePlaylist({
playlistId: newPlaylistId,
})
);
this.xtreamStore.fetchXtreamPlaylist();
this.xtreamStore.checkPortalStatus();
});
effect(
() => {
if (this.xtreamStore.currentPlaylist() !== null) {
@@ -59,15 +86,6 @@ export class XtreamShellComponent {
);
}
ngOnInit() {
this.xtreamStore.checkPortalStatus();
this.store.dispatch(
PlaylistActions.setActivePlaylist({
playlistId: this.route.snapshot.params.id,
})
);
}
handleCategoryClick(category: 'vod' | 'live' | 'series') {
this.xtreamStore.setSelectedContentType(category);
this.router.navigate([category], {
@@ -44,4 +44,5 @@ export interface XtreamState {
portalStatus: PortalStatusType;
globalSearchResults: GlobalSearchResult[];
streamUrl: string;
playlistId: string | null;
}
+82 -59
View File
@@ -1,5 +1,4 @@
import { computed, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import {
patchState,
signalStore,
@@ -60,6 +59,7 @@ const initialState: XtreamState = {
portalStatus: 'unavailable',
globalSearchResults: [],
streamUrl: null,
playlistId: null,
};
/** to decode epg */
@@ -200,7 +200,6 @@ export const XtreamStore = signalStore(
withMethods(
(
store,
route = inject(ActivatedRoute),
dataService = inject(DataService),
dbService = inject(DatabaseService),
oldStore = inject(Store),
@@ -215,7 +214,8 @@ export const XtreamStore = signalStore(
>,
type: 'live' | 'movies' | 'series'
) => {
console.log(`Fetching ${type} categories...`);
const playlistId = store.playlistId();
console.log(`Fetching ${type} categories for playlist ${playlistId}...`);
patchState(store, { isLoadingCategories: true });
const queryParams = {
action,
@@ -224,17 +224,14 @@ export const XtreamStore = signalStore(
};
return from(
dbService.hasXtreamCategories(
route.snapshot.params.id,
type
)
dbService.hasXtreamCategories(playlistId, type)
).pipe(
switchMap(async (exists) => {
try {
if (exists) {
const localData =
await dbService.getXtreamCategories(
route.snapshot.params.id,
playlistId,
type
);
patchState(store, {
@@ -253,7 +250,7 @@ export const XtreamStore = signalStore(
if (remoteData && Array.isArray(remoteData)) {
await dbService.saveXtreamCategories(
route.snapshot.params.id,
playlistId,
remoteData,
type
);
@@ -261,7 +258,7 @@ export const XtreamStore = signalStore(
const localData =
await dbService.getXtreamCategories(
route.snapshot.params.id,
playlistId,
type
);
patchState(store, {
@@ -290,7 +287,8 @@ export const XtreamStore = signalStore(
>,
type: 'live' | 'movie' | 'series'
) => {
console.log(`Fetching ${type} streams...`);
const playlistId = store.playlistId();
console.log(`Fetching ${type} streams for playlist ${playlistId}...`);
patchState(store, { isLoadingContent: true });
const queryParams = {
action,
@@ -299,14 +297,14 @@ export const XtreamStore = signalStore(
};
return from(
dbService.hasXtreamContent(route.snapshot.params.id, type)
dbService.hasXtreamContent(playlistId, type)
).pipe(
switchMap(async (exists) => {
try {
if (exists) {
const localData =
await dbService.getXtreamContent(
route.snapshot.params.id,
playlistId,
type
);
patchState(store, {
@@ -331,7 +329,7 @@ export const XtreamStore = signalStore(
});
const insertedCount =
await dbService.saveXtreamContent(
route.snapshot.params.id,
playlistId,
remoteData,
type,
(count) =>
@@ -343,7 +341,7 @@ export const XtreamStore = signalStore(
}
const localData = await dbService.getXtreamContent(
route.snapshot.params.id,
playlistId,
type
);
patchState(store, {
@@ -363,53 +361,57 @@ export const XtreamStore = signalStore(
const searchContent = async (
searchTerm: string,
types: string[],
route = inject(ActivatedRoute),
dbService = inject(DatabaseService)
types: string[]
) => {
if (!route.snapshot.params.id) return;
const playlistId = store.playlistId();
if (!playlistId) return;
const results = await dbService.searchXtreamContent(
route.snapshot.params.id,
playlistId,
searchTerm,
types
);
return results;
};
const fetchXtreamPlaylist = rxMethod<void>(
pipe(() => {
const playlistId = route.snapshot.params.id;
if (!playlistId) return EMPTY;
return from(dbService.getPlaylistById(playlistId)).pipe(
switchMap(async (playlist) => {
if (playlist) {
patchState(store, {
currentPlaylist: playlist,
});
} else {
const playlist = oldStore.selectSignal(
selectActivePlaylist
)() as Playlist;
if (playlist) {
await dbService.createPlaylist(playlist);
patchState(store, {
currentPlaylist: {
...playlist,
serverUrl: playlist.serverUrl,
id: playlist._id,
name: playlist.title,
},
});
}
}
}),
tap((res) => {
console.log('Fetched playlist:', res);
})
);
})
);
const fetchXtreamPlaylist = async () => {
const playlistId = store.playlistId();
console.log('fetchXtreamPlaylist called with playlistId:', playlistId);
if (!playlistId) {
console.log('No playlistId, skipping fetch');
return;
}
try {
const playlist = await dbService.getPlaylistById(playlistId);
console.log('Fetched playlist from DB:', playlist);
if (playlist) {
patchState(store, {
currentPlaylist: playlist,
});
} else {
const activePlaylist = oldStore.selectSignal(
selectActivePlaylist
)() as Playlist;
console.log('No playlist in DB, using active playlist:', activePlaylist);
if (activePlaylist) {
await dbService.createPlaylist(activePlaylist);
patchState(store, {
currentPlaylist: {
...activePlaylist,
serverUrl: activePlaylist.serverUrl,
id: activePlaylist._id,
name: activePlaylist.title,
},
});
}
}
} catch (error) {
console.error('Error fetching playlist:', error);
}
};
const fetchLiveCategories = rxMethod<void>(
pipe(
@@ -504,7 +506,7 @@ export const XtreamStore = signalStore(
}>(
pipe(
switchMap(({ term, types }) =>
from(searchContent(term, types, route, dbService)).pipe(
from(searchContent(term, types)).pipe(
tap((results) => {
patchState(store, {
searchResults: results || [],
@@ -645,9 +647,9 @@ export const XtreamStore = signalStore(
console.log('Content initialization completed');
// Check if we need to restore favorites and recently viewed after refresh
const playlistId = route.snapshot.params.id;
if (playlistId) {
const restoreKey = `xtream-restore-${playlistId}`;
const currentPlaylistId = store.playlistId();
if (currentPlaylistId) {
const restoreKey = `xtream-restore-${currentPlaylistId}`;
const restoreData = localStorage.getItem(restoreKey);
if (restoreData) {
try {
@@ -656,7 +658,7 @@ export const XtreamStore = signalStore(
recentlyViewedXtreamIds,
} = JSON.parse(restoreData);
await dbService.restoreXtreamUserData(
playlistId,
currentPlaylistId,
favoritedXtreamIds,
recentlyViewedXtreamIds
);
@@ -761,6 +763,27 @@ export const XtreamStore = signalStore(
fetchVodDetailsWithMetadata,
fetchSerialDetailsWithMetadata,
initializeContent,
/**
* Set the current playlist ID
*/
setPlaylistId(playlistId: string) {
patchState(store, { playlistId });
},
/**
* Reset store to initial state for switching between playlists
*/
resetStore(newPlaylistId?: string) {
patchState(store, {
...initialState,
playlistId: newPlaylistId ?? null,
limit: Number(
localStorage.getItem('xtream-page-size') ?? 25
),
hideExternalInfoDialog:
localStorage.getItem('hideExternalInfoDialog') ===
'true',
});
},
updatePlaylist(playlist: any) {
patchState(store, {
currentPlaylist: {
@@ -916,7 +939,7 @@ export const XtreamStore = signalStore(
* Reload categories from database after visibility changes
*/
async reloadCategories() {
const playlistId = route.snapshot.params.id;
const playlistId = store.playlistId();
if (!playlistId) return;
try {
@@ -9,6 +9,7 @@ import {
signal,
viewChild,
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormsModule } from '@angular/forms';
import { MatIconButton } from '@angular/material/button';
import { MatIcon } from '@angular/material/icon';
@@ -90,7 +91,10 @@ export class PlaylistSwitcherComponent {
// Listen for route changes
this.router.events
.pipe(filter((event) => event instanceof NavigationEnd))
.pipe(
filter((event) => event instanceof NavigationEnd),
takeUntilDestroyed()
)
.subscribe((event: NavigationEnd) => {
this.updateActivePlaylistFromRoute(event.urlAfterRedirects);
});
@@ -113,21 +117,23 @@ export class PlaylistSwitcherComponent {
}
private async checkPortalStatuses(playlists: PlaylistMeta[]) {
const statusMap = new Map<string, PortalStatus>();
for (const playlist of playlists) {
if (playlist.serverUrl && playlist.username && playlist.password) {
const statusPromises = playlists
.filter(playlist => playlist.serverUrl && playlist.username && playlist.password)
.map(async (playlist) => {
try {
const status = await this.portalStatusService.checkPortalStatus(
playlist.serverUrl,
playlist.username,
playlist.password
);
statusMap.set(playlist._id, status);
return { id: playlist._id, status };
} catch {
statusMap.set(playlist._id, 'unavailable');
return { id: playlist._id, status: 'unavailable' as PortalStatus };
}
}
}
});
const results = await Promise.all(statusPromises);
const statusMap = new Map(results.map(r => [r.id, r.status]));
this.portalStatuses.set(statusMap);
}
@@ -147,8 +153,8 @@ export class PlaylistSwitcherComponent {
this.router.navigate(['stalker', playlist._id]);
} else {
this.router.navigate(['playlists', playlist._id]);
this.playlistSelected.emit(playlist._id);
}
this.playlistSelected.emit(playlist._id);
}
getPlaylistIcon(playlist: PlaylistMeta): string {