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()) { - - } - @if (contextMenuChannel()?.m3uChannel && !contextMenuUnavailable()) { - - } - @if (contextMenuCanOpenInPlaylist()) { - - } - @if (mode() === 'recent') { - - } - + + 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 @@ - - - - - +