feat(stalker): add radio support

Entire-Checkpoint: f957cd9849e0
This commit is contained in:
4gray committed 2026-05-09 01:28:39 +02:00
1 parent e9875f9f29
commit ab905af285
59 files changed
+1059 -179

No files matched your search

@@ -14,6 +14,7 @@ import { WorkspaceContextPanelComponent } from './workspace-context-panel.compon
const translations: Record<string, string> = {
'WORKSPACE.CONTEXT.MANAGE_CATEGORIES': 'Manage categories',
'WORKSPACE.CONTEXT.RADIO_CATEGORIES': 'Radio Categories',
'WORKSPACE.CONTEXT.XTREAM_SYNCING_LIVE': 'Syncing live categories...',
'WORKSPACE.CONTEXT.XTREAM_SYNCING_MOVIES': 'Syncing movie categories...',
'WORKSPACE.SHELL.XTREAM_IMPORT_LOADING':
@@ -53,7 +54,9 @@ describe('WorkspaceContextPanelComponent', () => {
reloadCategories: jest.fn(),
};
const stalkerStore = {
getCategoryResource: signal([]),
getCategoryResource: signal<
Array<{ category_id: string; category_name: string }>
>([]),
selectedCategoryId: signal<string | null>(null),
isCategoryResourceLoading: signal(false),
isCategoryResourceFailed: signal(false),
@@ -82,6 +85,11 @@ describe('WorkspaceContextPanelComponent', () => {
xtreamStore.setSelectedItem.mockClear();
xtreamStore.setSelectedCategory.mockClear();
xtreamStore.reloadCategories.mockClear();
stalkerStore.getCategoryResource.set([]);
stalkerStore.selectedCategoryId.set(null);
stalkerStore.setSelectedCategory.mockClear();
stalkerStore.setPage.mockClear();
stalkerStore.clearSelectedItem.mockClear();
router.navigate.mockClear();
dialog.open.mockClear();
@@ -217,4 +225,32 @@ describe('WorkspaceContextPanelComponent', () => {
2,
]);
});
it('labels Stalker radio categories and keeps category selection in the radio layout', () => {
fixture.componentRef.setInput('context', {
provider: 'stalker',
playlistId: 'stalker-1',
});
fixture.componentRef.setInput('section', 'radio');
stalkerStore.getCategoryResource.set([
{ category_id: '*', category_name: 'All radio' },
]);
fixture.detectChanges();
const title = fixture.nativeElement.querySelector(
'.context-header h2'
) as HTMLElement;
const categoryButton = fixture.nativeElement.querySelector(
'.category-item'
) as HTMLButtonElement;
expect(title.textContent).toContain('Radio Categories');
categoryButton.click();
expect(stalkerStore.setSelectedCategory).toHaveBeenCalledWith('*');
expect(stalkerStore.setPage).toHaveBeenCalledWith(0);
expect(stalkerStore.clearSelectedItem).toHaveBeenCalled();
expect(router.navigate).not.toHaveBeenCalled();
});
});
@@ -69,7 +69,8 @@ export class WorkspaceContextPanelComponent {
this.context().provider === 'stalker' &&
(this.section() === 'vod' ||
this.section() === 'series' ||
this.section() === 'itv')
this.section() === 'itv' ||
this.section() === 'radio')
);
readonly xtreamCategories = this.xtreamStore.getCategoriesBySelectedType;
@@ -192,6 +193,9 @@ export class WorkspaceContextPanelComponent {
if (this.section() === 'series') {
return 'WORKSPACE.CONTEXT.SERIES_CATEGORIES';
}
if (this.section() === 'radio') {
return 'WORKSPACE.CONTEXT.RADIO_CATEGORIES';
}
return 'WORKSPACE.CONTEXT.LIVE_CATEGORIES';
}
@@ -319,7 +323,7 @@ export class WorkspaceContextPanelComponent {
this.stalkerStore.setPage(0);
this.stalkerStore.clearSelectedItem();
if (section === 'itv') {
if (section === 'itv' || section === 'radio') {
return;
}
@@ -19,7 +19,11 @@ import { WorkspaceContextPanelComponent } from '../../../workspace-context-panel
import { WorkspaceSettingsContextPanelComponent } from '../../../workspace-context-panel/workspace-settings-context-panel.component';
import { WorkspaceSourcesFiltersPanelComponent } from '../../../workspace-sources-filters-panel/workspace-sources-filters-panel.component';
const LIVE_SECTIONS: ReadonlySet<PortalRailSection> = new Set(['live', 'itv']);
const LIVE_SECTIONS: ReadonlySet<PortalRailSection> = new Set([
'live',
'itv',
'radio',
]);
@Component({
selector: 'app-workspace-shell-context-sidebar',
@@ -279,6 +279,18 @@ function getPortalNavigationCommands(
section: PortalRailSection
): WorkspaceCommandContribution[] {
const liveSection = context.provider === 'stalker' ? 'itv' : 'live';
const radioCommand =
context.provider === 'stalker'
? createNavigationCommand(ctx, {
id: 'go-to-radio',
context,
targetSection: 'radio',
currentSection: section,
icon: 'radio',
labelKey: 'WORKSPACE.SHELL.RAIL_RADIO',
priority: 115,
})
: null;
return [
createNavigationCommand(ctx, {
@@ -299,6 +311,7 @@ function getPortalNavigationCommands(
labelKey: 'WORKSPACE.SHELL.RAIL_LIVE',
priority: 110,
}),
radioCommand,
createNavigationCommand(ctx, {
id: 'go-to-series',
context,
@@ -24,6 +24,7 @@ export const RAIL_TOOLTIP_KEYS: Readonly<
vod: 'WORKSPACE.SHELL.RAIL_MOVIES',
live: 'WORKSPACE.SHELL.RAIL_LIVE',
itv: 'WORKSPACE.SHELL.RAIL_LIVE',
radio: 'WORKSPACE.SHELL.RAIL_RADIO',
series: 'WORKSPACE.SHELL.RAIL_SERIES',
'recently-added': 'WORKSPACE.SHELL.RAIL_RECENTLY_ADDED',
search: 'WORKSPACE.SHELL.RAIL_SEARCH',
@@ -81,7 +81,8 @@ export function resolveSearchPlaceholderKey(
(section === 'vod' ||
section === 'series' ||
section === 'live' ||
section === 'itv')
section === 'itv' ||
section === 'radio')
) {
return SEARCH_SECTION_PLACEHOLDER;
}
@@ -145,7 +146,8 @@ export function resolveSearchScopeLabel(ctx: SearchScopeContext): string {
section === 'vod' ||
section === 'series' ||
section === 'live' ||
section === 'itv'
section === 'itv' ||
section === 'radio'
) {
const categoryLabel = resolveActiveCategoryLabel(ctx);
const sectionLabel = translateRailSection(section, translate);
@@ -541,6 +541,19 @@ describe('WorkspaceShellFacade', () => {
);
});
it('treats stalker radio search as a remote section search', () => {
facade.currentUrl.set('/workspace/stalker/pl-1/radio?q=jazz');
(facade as { syncSearchFromRoute: () => void }).syncSearchFromRoute();
TestBed.flushEffects();
expect(stalkerStore.setSearchPhrase).toHaveBeenCalledWith('jazz');
expect(facade.canUseSearch()).toBe(true);
expect(facade.searchScopeLabel()).toBe(
'WORKSPACE.SHELL.RAIL_RADIO / All Items'
);
expect(facade.searchStatusLabel()).toBe('');
});
it('applies q to Xtream category search on vod routes', () => {
const xtreamStore = TestBed.inject(
XtreamStore