fix(xtream): keep live playback on category switch (#936)

This commit is contained in:
4gray authored and GitHub committed 2026-05-14 20:52:02 +02:00
1 parent db08ec2958
commit 2a5b1d1da7
4 files changed
+114 -3

No files matched your search

@@ -525,6 +525,14 @@ test.describe('Electron Workspace Search', () => {
app.mainWindow,
`Live TV / ${sample.categoryName}`
);
await expect(
app.mainWindow.locator(
'app-live-stream-layout .content-container .video-player'
)
).toBeVisible({ timeout: 20000 });
await expect(
channelItemByTitle(app.mainWindow, sample.targetTitle).first()
).toHaveClass(/(^|\s)active(\s|$)/);
await fillWorkspaceSearch(app.mainWindow, sample.targetTitle);
await expectPathname(
@@ -0,0 +1,14 @@
import type { ActivatedRouteSnapshot } from '@angular/router';
export function hasActiveLiveCategoryRoute(
route: ActivatedRouteSnapshot
): boolean {
if (
route.routeConfig?.path === 'live/:categoryId' &&
route.paramMap.has('categoryId')
) {
return true;
}
return route.children.some((child) => hasActiveLiveCategoryRoute(child));
}
@@ -21,6 +21,27 @@ const translations: Record<string, string> = {
'Fetching playlist data from source...',
};
interface RouteSnapshotStub {
routeConfig: { path: string } | null;
paramMap: { has: (param: string) => boolean };
children: RouteSnapshotStub[];
}
function createRouteSnapshot(
path: string | null,
hasCategoryId = false,
children: RouteSnapshotStub[] = []
): RouteSnapshotStub {
return {
routeConfig: path ? { path } : null,
paramMap: {
has: (param: string) =>
hasCategoryId && param === 'categoryId',
},
children,
};
}
describe('WorkspaceContextPanelComponent', () => {
let fixture: ComponentFixture<WorkspaceContextPanelComponent>;
const xtreamCategories = signal([
@@ -65,6 +86,11 @@ describe('WorkspaceContextPanelComponent', () => {
clearSelectedItem: jest.fn(),
};
const router = {
routerState: {
snapshot: {
root: createRouteSnapshot(null),
},
},
navigate: jest.fn(),
};
const dialog = {
@@ -90,6 +116,9 @@ describe('WorkspaceContextPanelComponent', () => {
stalkerStore.setSelectedCategory.mockClear();
stalkerStore.setPage.mockClear();
stalkerStore.clearSelectedItem.mockClear();
router.routerState.snapshot.root = createRouteSnapshot(null, false, [
createRouteSnapshot('live'),
]);
router.navigate.mockClear();
dialog.open.mockClear();
@@ -226,6 +255,47 @@ describe('WorkspaceContextPanelComponent', () => {
]);
});
it('preserves the active xtream live item when switching live categories', () => {
fixture.componentRef.setInput('section', 'live');
xtreamSelectedTypeContentState.set('ready');
fixture.detectChanges();
const categoryButtons = Array.from(
fixture.nativeElement.querySelectorAll('.category-item')
) as HTMLButtonElement[];
categoryButtons[1]?.click();
expect(xtreamStore.setSelectedCategory).toHaveBeenCalledWith(2);
expect(xtreamStore.setSelectedItem).not.toHaveBeenCalled();
expect(router.navigate).not.toHaveBeenCalled();
});
it('updates the live category URL without clearing playback when already deep linked', () => {
fixture.componentRef.setInput('section', 'live');
router.routerState.snapshot.root = createRouteSnapshot(null, false, [
createRouteSnapshot('live/:categoryId', true),
]);
xtreamSelectedTypeContentState.set('ready');
fixture.detectChanges();
const categoryButtons = Array.from(
fixture.nativeElement.querySelectorAll('.category-item')
) as HTMLButtonElement[];
categoryButtons[1]?.click();
expect(xtreamStore.setSelectedCategory).toHaveBeenCalledWith(2);
expect(xtreamStore.setSelectedItem).not.toHaveBeenCalled();
expect(router.navigate).toHaveBeenCalledWith(
['/workspace', 'xtreams', 'playlist-1', 'live', 2],
{
queryParamsHandling: 'preserve',
replaceUrl: true,
}
);
});
it('labels Stalker radio categories and keeps category selection in the radio layout', () => {
fixture.componentRef.setInput('context', {
provider: 'stalker',
@@ -20,6 +20,7 @@ import { StalkerStore } from '@iptvnator/portal/stalker/data-access';
import { XtreamStore } from '@iptvnator/portal/xtream/data-access';
import { WorkspaceContextCategoryViewComponent } from './components/workspace-context-category-view.component';
import { WorkspaceContextErrorViewComponent } from './components/workspace-context-error-view.component';
import { hasActiveLiveCategoryRoute } from './workspace-context-panel-route.utils';
type WorkspaceProvider = 'xtreams' | 'stalker' | 'playlists';
@@ -297,13 +298,31 @@ export class WorkspaceContextPanelComponent {
}
const categoryId = numericCategoryId;
this.xtreamStore.setSelectedItem(null);
this.xtreamStore.setSelectedCategory(categoryId);
if (section === 'live') {
this.xtreamStore.setSelectedCategory(categoryId);
const liveRouteHasCategory = hasActiveLiveCategoryRoute(
this.router.routerState.snapshot.root
);
if (liveRouteHasCategory) {
void this.router.navigate(
[
'/workspace',
'xtreams',
context.playlistId,
'live',
categoryId,
],
{
queryParamsHandling: 'preserve',
replaceUrl: true,
}
);
}
return;
}
this.xtreamStore.setSelectedItem(null);
this.xtreamStore.setSelectedCategory(categoryId);
this.router.navigate([
'/workspace',
'xtreams',