fix(sources): decode nested URLs before classifying; narrow Xtream paths

- A URL nested in a parameter or path segment is percent-decoded one
  round at a time until it reads as a URL or parameter list, so one
  encoded twice is cleaned too; a value still encoded after the last
  round is dropped. The name check unwraps nested values the same way.
- The two segments after `/live/` and its siblings count as an account
  only when a stream follows them, so a playlist path such as
  `/live/news/list.m3u` keeps its file name in the URL and the title.
- Adding a playlist from the phone drawer closes the drawer first, so
  two focus-trapped modals never stack.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Opus 5.5 committed 2026-10-13 23:04:07 +02:00
1 parent ea5bca7e96
commit d2b0325138
6 files changed
+155 -25

No files matched your search

@@ -7,7 +7,10 @@ import { SourceStatusService } from '@iptvnator/portal/shared/data-access';
import { SourceRemovalService } from '@iptvnator/playlist/shared/ui';
import { SourcesViewStateService } from '@iptvnator/playlist/shared/util';
import { PlaylistMeta } from '@iptvnator/shared/interfaces';
import { WORKSPACE_SHELL_ACTIONS } from '@iptvnator/workspace/shell/util';
import {
WORKSPACE_SHELL_ACTIONS,
WorkspaceShellContextDrawerService,
} from '@iptvnator/workspace/shell/util';
import { WorkspaceSourcesFiltersPanelComponent } from './workspace-sources-filters-panel.component';
const PLAYLISTS = [
@@ -137,7 +140,13 @@ describe('WorkspaceSourcesFiltersPanelComponent', () => {
expect(rows(element)).toHaveLength(4);
});
it('adds a playlist from the header', () => {
it('adds a playlist from the header, closing the phone drawer first', () => {
const drawer = TestBed.inject(WorkspaceShellContextDrawerService);
drawer.open();
let drawerOpenAtDialog: boolean | null = null;
openAddPlaylistDialog.mockImplementation(() => {
drawerOpenAtDialog = drawer.isOpen();
});
const element = render();
element
.querySelector<HTMLButtonElement>(
@@ -145,5 +154,7 @@ describe('WorkspaceSourcesFiltersPanelComponent', () => {
)
?.click();
expect(openAddPlaylistDialog).toHaveBeenCalled();
// Two focus-trapped modals must not stack.
expect(drawerOpenAtDialog).toBe(false);
});
});
@@ -137,6 +137,9 @@ export class WorkspaceSourcesFiltersPanelComponent {
}
addPlaylist(): void {
// On a phone the drawer is a focus-trapped modal of its own: it must
// not stay open under the dialog.
this.contextDrawer?.close();
this.workspaceActions?.openAddPlaylistDialog();
}
}