diff --git a/.changes/ui-channel-context-menu.md b/.changes/ui-channel-context-menu.md
new file mode 100644
index 000000000..1cabfc0e7
--- /dev/null
+++ b/.changes/ui-channel-context-menu.md
@@ -0,0 +1,7 @@
+---
+type: fix
+area: ui
+---
+
+The right-click menu on a channel looks and behaves the same in every channel
+list, and "Remove from history" is marked in red after a separator.
diff --git a/docs/architecture/iptvnator-ui-guidelines.md b/docs/architecture/iptvnator-ui-guidelines.md
index 66a6c1394..7226adbd2 100644
--- a/docs/architecture/iptvnator-ui-guidelines.md
+++ b/docs/architecture/iptvnator-ui-guidelines.md
@@ -515,6 +515,14 @@ dialogs, player controls and hidden/inert panes keep their own behavior.
The shared row should be reused instead of rebuilding channel markup per view.
+Its right-click menu is `app-channel-context-menu` (`@iptvnator/ui/components`),
+shared by the M3U views, the Xtream and Stalker live lists and the unified
+favorites and recents. A host sets which rows apply (`showMapEpg`,
+`showDetails`, `openInPlaylistName`, `showRemoveFromHistory`), handles their
+outputs, and calls `open(event)` from the row's `contextMenuRequested`; the
+rows keep one order and Remove from history is the danger row after a
+divider.
+
### Current Reference Values
- Current minimum height:
diff --git a/libs/portal/shared/ui/src/lib/components/global-favorites-list/global-favorites-list.component.html b/libs/portal/shared/ui/src/lib/components/global-favorites-list/global-favorites-list.component.html
index 93d680eb2..e69b8bbac 100644
--- a/libs/portal/shared/ui/src/lib/components/global-favorites-list/global-favorites-list.component.html
+++ b/libs/portal/shared/ui/src/lib/components/global-favorites-list/global-favorites-list.component.html
@@ -63,47 +63,19 @@
}
-
-
-
- @if (supportsEpgMapping && !contextMenuUnavailable()) {
-
- settings_remote
- {{ 'CHANNELS.MAP_EPG' | translate }}
-
- }
- @if (contextMenuChannel()?.m3uChannel && !contextMenuUnavailable()) {
-
- info
- {{ 'CHANNELS.SHOW_DETAILS' | translate }}
-
- }
- @if (contextMenuCanOpenInPlaylist()) {
-
- open_in_new
- {{
- 'PORTALS.VIEW_IN_PORTAL_TOOLTIP'
- | translate: { name: contextMenuPlaylistName() }
- }}
-
- }
- @if (mode() === 'recent') {
-
- delete
-
- {{ 'WORKSPACE.DASHBOARD.REMOVE_FROM_HISTORY' | translate }}
-
-
- }
-
+
+
diff --git a/libs/portal/shared/ui/src/lib/components/global-favorites-list/global-favorites-list.component.scss b/libs/portal/shared/ui/src/lib/components/global-favorites-list/global-favorites-list.component.scss
index e22cb4a93..3bb74e868 100644
--- a/libs/portal/shared/ui/src/lib/components/global-favorites-list/global-favorites-list.component.scss
+++ b/libs/portal/shared/ui/src/lib/components/global-favorites-list/global-favorites-list.component.scss
@@ -40,14 +40,6 @@
gap: 2px;
}
-.channel-context-menu-anchor {
- position: fixed;
- width: 0;
- height: 0;
- opacity: 0;
- pointer-events: none;
-}
-
.empty-state {
display: flex;
flex-direction: column;
diff --git a/libs/portal/shared/ui/src/lib/components/global-favorites-list/global-favorites-list.component.spec.ts b/libs/portal/shared/ui/src/lib/components/global-favorites-list/global-favorites-list.component.spec.ts
index 4610a349b..17588f193 100644
--- a/libs/portal/shared/ui/src/lib/components/global-favorites-list/global-favorites-list.component.spec.ts
+++ b/libs/portal/shared/ui/src/lib/components/global-favorites-list/global-favorites-list.component.spec.ts
@@ -191,7 +191,7 @@ describe('GlobalFavoritesListComponent', () => {
fixture.detectChanges();
const openMenuSpy = jest
- .spyOn(fixture.componentInstance.contextMenuTrigger(), 'openMenu')
+ .spyOn(fixture.componentInstance.channelContextMenu(), 'open')
.mockImplementation();
fixture.componentInstance.onChannelContextMenu(
@@ -203,11 +203,9 @@ describe('GlobalFavoritesListComponent', () => {
);
await Promise.resolve();
- expect(fixture.componentInstance.contextMenuPosition()).toEqual({
- x: '24px',
- y: '32px',
- });
- expect(openMenuSpy).toHaveBeenCalled();
+ expect(openMenuSpy).toHaveBeenCalledWith(
+ expect.objectContaining({ clientX: 24, clientY: 32 })
+ );
fixture.componentInstance.openChannelDetails();
@@ -230,8 +228,8 @@ describe('GlobalFavoritesListComponent', () => {
fixture.detectChanges();
jest.spyOn(
- fixture.componentInstance.contextMenuTrigger(),
- 'openMenu'
+ fixture.componentInstance.channelContextMenu(),
+ 'open'
).mockImplementation();
fixture.componentInstance.onChannelContextMenu(
@@ -266,8 +264,8 @@ describe('GlobalFavoritesListComponent', () => {
expect(fixture.componentInstance.hasChannelContextMenu(row)).toBe(true);
jest.spyOn(
- fixture.componentInstance.contextMenuTrigger(),
- 'openMenu'
+ fixture.componentInstance.channelContextMenu(),
+ 'open'
).mockImplementation();
fixture.componentInstance.onChannelContextMenu(
fixture.componentInstance.enrichedChannels()[0],
@@ -293,11 +291,11 @@ describe('GlobalFavoritesListComponent', () => {
expect(fixture.componentInstance.hasChannelContextMenu(row)).toBe(true);
const closeMenu = jest
- .spyOn(fixture.componentInstance.contextMenuTrigger(), 'closeMenu')
+ .spyOn(fixture.componentInstance.channelContextMenu(), 'close')
.mockImplementation();
jest.spyOn(
- fixture.componentInstance.contextMenuTrigger(),
- 'openMenu'
+ fixture.componentInstance.channelContextMenu(),
+ 'open'
).mockImplementation();
fixture.componentInstance.onChannelContextMenu(
diff --git a/libs/portal/shared/ui/src/lib/components/global-favorites-list/global-favorites-list.component.ts b/libs/portal/shared/ui/src/lib/components/global-favorites-list/global-favorites-list.component.ts
index 134449b4c..003bc67a4 100644
--- a/libs/portal/shared/ui/src/lib/components/global-favorites-list/global-favorites-list.component.ts
+++ b/libs/portal/shared/ui/src/lib/components/global-favorites-list/global-favorites-list.component.ts
@@ -1,5 +1,8 @@
import { isCatalogUnavailable } from '@iptvnator/shared/interfaces';
-import { ChannelScrollFocusDirective } from '@iptvnator/ui/components';
+import {
+ ChannelContextMenuComponent,
+ ChannelScrollFocusDirective,
+} from '@iptvnator/ui/components';
import {
CdkDragDrop,
DragDropModule,
@@ -17,7 +20,6 @@ import {
} from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
-import { MatMenuModule, MatMenuTrigger } from '@angular/material/menu';
import {
ChannelDetailsDialogComponent,
ChannelListItemComponent,
@@ -57,11 +59,11 @@ export type GlobalFavoritesListMode = 'favorites' | 'recent';
styleUrl: './global-favorites-list.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
+ ChannelContextMenuComponent,
ChannelScrollFocusDirective,
ChannelListItemComponent,
DragDropModule,
MatIconModule,
- MatMenuModule,
TranslateModule,
],
})
@@ -72,8 +74,9 @@ export class GlobalFavoritesListComponent {
private readonly runtime = inject(RuntimeCapabilitiesService);
private readonly settingsStore = inject(SettingsStore);
- readonly contextMenuTrigger =
- viewChild.required('contextMenuTrigger');
+ readonly channelContextMenu = viewChild.required(
+ ChannelContextMenuComponent
+ );
readonly openStreamOnDoubleClick = computed(() =>
this.settingsStore.openStreamOnDoubleClick()
);
@@ -113,10 +116,6 @@ export class GlobalFavoritesListComponent {
readonly contextMenuPlaylistName = computed(() =>
playlistDisplayLabel(this.contextMenuChannel()?.playlistName)
);
- readonly contextMenuPosition = signal({
- x: '0px',
- y: '0px',
- });
readonly isCustomSort = computed(() => this.sortMode() === 'custom');
readonly canDragDrop = computed(
@@ -197,19 +196,7 @@ export class GlobalFavoritesListComponent {
event: MouseEvent
): void {
this.contextMenuChannel.set(channel);
- this.contextMenuPosition.set({
- x: `${event.clientX}px`,
- y: `${event.clientY}px`,
- });
-
- const trigger = this.contextMenuTrigger();
- if (trigger.menuOpen) {
- trigger.closeMenu();
- }
-
- queueMicrotask(() => {
- this.contextMenuTrigger().openMenu();
- });
+ this.channelContextMenu().open(event);
}
hasChannelContextMenu(channel: UnifiedFavoriteChannel): boolean {
@@ -235,7 +222,7 @@ export class GlobalFavoritesListComponent {
return;
}
- this.contextMenuTrigger().closeMenu();
+ this.channelContextMenu().close();
this.openInPlaylistRequested.emit(channel);
}
@@ -245,7 +232,7 @@ export class GlobalFavoritesListComponent {
return;
}
- this.contextMenuTrigger().closeMenu();
+ this.channelContextMenu().close();
const stalkerId = this.stalkerItemId(item);
const channelKey = item.m3uChannel
? resolveChannelEpgLookupKey(item.m3uChannel)
@@ -308,7 +295,7 @@ export class GlobalFavoritesListComponent {
return;
}
- this.contextMenuTrigger().closeMenu();
+ this.channelContextMenu().close();
this.dialog.open(ChannelDetailsDialogComponent, {
data: channel,
maxWidth: '720px',
@@ -322,7 +309,7 @@ export class GlobalFavoritesListComponent {
return;
}
- this.contextMenuTrigger().closeMenu();
+ this.channelContextMenu().close();
this.removeRequested.emit(channel);
}
diff --git a/libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.html b/libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.html
index cf7dedeca..440a4dd6c 100644
--- a/libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.html
+++ b/libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.html
@@ -244,21 +244,7 @@
-
-
-
-
- settings_remote
- {{ 'CHANNELS.MAP_EPG' | translate }}
-
-
+