diff --git a/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.html b/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.html
index 189343fc2..83b04d635 100644
--- a/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.html
+++ b/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.html
@@ -1,16 +1,22 @@
}
} @else {
-
- live_tv
-
- {{ 'CHANNELS.SELECT_CHANNEL_PLAYBACK' | translate }}
-
-
+
}
diff --git a/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.scss b/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.scss
index 307fedf4c..924b1303d 100644
--- a/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.scss
+++ b/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.scss
@@ -82,32 +82,6 @@
}
}
-.empty-state-container {
- display: flex;
- flex: 1;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- text-align: center;
- color: rgba(15, 23, 42, 0.45);
-
- .empty-state-icon {
- width: 72px;
- height: 72px;
- margin-bottom: 24px;
- font-size: 72px;
- opacity: 0.25;
- }
-
- .empty-state-title {
- margin: 0;
- font-family: 'DM Sans', sans-serif;
- font-size: 1.25rem;
- font-weight: 500;
- opacity: 0.7;
- }
-}
-
@media only screen and (max-width: 599px) {
:host {
flex-direction: column-reverse;
diff --git a/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.spec.ts b/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.spec.ts
index 837fed7ee..1f76c6f2d 100644
--- a/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.spec.ts
+++ b/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.spec.ts
@@ -140,6 +140,7 @@ describe('VideoPlayerComponent', () => {
beforeEach(async () => {
syncStoreState(null);
+ localStorage.removeItem('m3u-sidebar-width');
player.set(VideoPlayer.VideoJs);
showCaptions.set(false);
currentEpgProgram.set(null);
@@ -285,11 +286,74 @@ describe('VideoPlayerComponent', () => {
).not.toBeNull();
});
- it('renders the empty state when no channel is active', () => {
+ it('updates the outer sidebar width while grouped view requests a larger total width', () => {
+ fixture.detectChanges();
+
+ component.onGroupedSidebarWidthRequested(540);
+ fixture.detectChanges();
+
+ const sidebar = fixture.nativeElement.querySelector(
+ '.sidebar'
+ ) as HTMLElement | null;
+
+ expect(sidebar?.style.width).toBe('540px');
+ });
+
+ it('uses the single-pane sidebar key for all-channel view instead of the groups total key', () => {
+ fixture.destroy();
+
+ localStorage.setItem('m3u-sidebar-width', '320');
+ localStorage.setItem('m3u-groups-sidebar-width', '560');
+
+ fixture = TestBed.createComponent(VideoPlayerComponent);
+ component = fixture.componentInstance;
+ headerContext = TestBed.inject(WorkspaceHeaderContextService);
+
+ fixture.detectChanges();
+
+ const sidebar = fixture.nativeElement.querySelector(
+ '.sidebar'
+ ) as HTMLElement | null;
+
+ expect(sidebar?.style.width).toBe('320px');
+ });
+
+ it('tracks manual sidebar resize and persists the committed width', () => {
+ fixture.detectChanges();
+
+ component.onSidebarWidthChange(420);
+ fixture.detectChanges();
+
+ const sidebar = fixture.nativeElement.querySelector(
+ '.sidebar'
+ ) as HTMLElement | null;
+
+ expect(sidebar?.style.width).toBe('420px');
+
+ component.onSidebarResizeEnd(420);
+
+ expect(localStorage.getItem('m3u-sidebar-width')).toBe('420');
+ });
+
+ it('clamps and persists grouped-view sidebar width requests on resize end', () => {
+ fixture.detectChanges();
+
+ component.onGroupedSidebarWidthRequestEnded(640);
+ fixture.detectChanges();
+
+ const sidebar = fixture.nativeElement.querySelector(
+ '.sidebar'
+ ) as HTMLElement | null;
+
+ expect(sidebar?.style.width).toBe('600px');
+ expect(localStorage.getItem('m3u-sidebar-width')).toBe('600');
+ });
+
+ it('renders the shared empty state when no channel is active', () => {
fixture.detectChanges();
const emptyState = fixture.nativeElement.querySelector(
- '.empty-state-container'
+ 'app-portal-empty-state'
) as HTMLElement | null;
expect(emptyState).not.toBeNull();
diff --git a/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.ts b/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.ts
index 88581d0a9..9bf871527 100644
--- a/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.ts
+++ b/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.ts
@@ -7,11 +7,12 @@ import {
Injector,
OnDestroy,
OnInit,
+ computed,
effect,
inject,
+ signal,
} from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
-import { MatIcon } from '@angular/material/icon';
import { ActivatedRoute, Router } from '@angular/router';
import { Store } from '@ngrx/store';
import { StorageMap } from '@ngx-pwa/local-storage';
@@ -50,6 +51,7 @@ import {
PORTAL_EXTERNAL_PLAYBACK,
WorkspaceHeaderContextService,
} from '@iptvnator/portal/shared/util';
+import { PortalEmptyStateComponent } from '@iptvnator/portal/shared/ui';
import {
ArtPlayerComponent,
AudioPlayerComponent,
@@ -71,6 +73,11 @@ import {
} from 'shared-interfaces';
const M3U_MULTI_EPG_HEADER_ACTION_ID = 'm3u-multi-epg';
+const M3U_SIDEBAR_STORAGE_KEY = 'm3u-sidebar-width';
+const M3U_GROUPS_SIDEBAR_STORAGE_KEY = 'm3u-groups-sidebar-width';
+const M3U_SIDEBAR_MIN_WIDTH = 200;
+const M3U_SIDEBAR_MAX_WIDTH = 600;
+const M3U_SIDEBAR_DEFAULT_WIDTH = 460;
@Component({
selector: 'app-video-player',
@@ -81,7 +88,7 @@ const M3U_MULTI_EPG_HEADER_ACTION_ID = 'm3u-multi-epg';
CommonModule,
EpgListComponent,
HtmlVideoPlayerComponent,
- MatIcon,
+ PortalEmptyStateComponent,
ResizableDirective,
SidebarComponent,
TranslatePipe,
@@ -109,6 +116,14 @@ export class VideoPlayerComponent implements OnInit, OnDestroy {
readonly activeChannel = this.store.selectSignal(selectActive);
readonly activePlaylistId = this.playlistContext.resolvedPlaylistId;
readonly channels = this.store.selectSignal(selectChannels);
+ readonly sidebarStorageKey = computed(() =>
+ this.activeView() === 'groups'
+ ? M3U_GROUPS_SIDEBAR_STORAGE_KEY
+ : M3U_SIDEBAR_STORAGE_KEY
+ );
+ readonly sidebarWidth = signal(M3U_SIDEBAR_DEFAULT_WIDTH);
+ readonly sidebarMinWidth = M3U_SIDEBAR_MIN_WIDTH;
+ readonly sidebarMaxWidth = M3U_SIDEBAR_MAX_WIDTH;
/** Channels list */
readonly channels$: Observable = this.store.select(
@@ -168,6 +183,12 @@ export class VideoPlayerComponent implements OnInit, OnDestroy {
};
});
+ effect(() => {
+ this.sidebarWidth.set(
+ this.loadSidebarWidth(this.sidebarStorageKey())
+ );
+ });
+
effect(() => {
const playlistId = this.activePlaylistId();
const activeChannel = this.activeChannel();
@@ -361,6 +382,22 @@ export class VideoPlayerComponent implements OnInit, OnDestroy {
this.statusSubscription?.unsubscribe();
}
+ onSidebarWidthChange(width: number): void {
+ this.sidebarWidth.set(this.clampSidebarWidth(width));
+ }
+
+ onSidebarResizeEnd(width: number): void {
+ this.persistSidebarWidth(this.sidebarStorageKey(), width);
+ }
+
+ onGroupedSidebarWidthRequested(width: number): void {
+ this.sidebarWidth.set(this.clampSidebarWidth(width));
+ }
+
+ onGroupedSidebarWidthRequestEnded(width: number): void {
+ this.persistSidebarWidth(this.sidebarStorageKey(), width);
+ }
+
/**
* Opens a playlist provided as a url param
* e.g. iptvnat.or?url=http://...
@@ -393,6 +430,38 @@ export class VideoPlayerComponent implements OnInit, OnDestroy {
});
}
+ private loadSidebarWidth(storageKey: string): number {
+ const fallbackKey =
+ storageKey === M3U_GROUPS_SIDEBAR_STORAGE_KEY
+ ? M3U_SIDEBAR_STORAGE_KEY
+ : '';
+ const storedWidth = Number.parseInt(
+ localStorage.getItem(storageKey) ??
+ (fallbackKey ? localStorage.getItem(fallbackKey) : '') ??
+ '',
+ 10
+ );
+
+ return this.clampSidebarWidth(
+ Number.isNaN(storedWidth)
+ ? M3U_SIDEBAR_DEFAULT_WIDTH
+ : storedWidth
+ );
+ }
+
+ private persistSidebarWidth(storageKey: string, width: number): void {
+ const clampedWidth = this.clampSidebarWidth(width);
+ this.sidebarWidth.set(clampedWidth);
+ localStorage.setItem(storageKey, clampedWidth.toString());
+ }
+
+ private clampSidebarWidth(width: number): number {
+ return Math.max(
+ M3U_SIDEBAR_MIN_WIDTH,
+ Math.min(M3U_SIDEBAR_MAX_WIDTH, width)
+ );
+ }
+
private async persistRecentlyViewedChannel(
playlistId: string,
channel: Channel