mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-13 04:26:16 -08:00
refactor: add lifecycle-safe router unsubscribe and optimize portal checks
This commit is contained in:
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;
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user