diff --git a/apps/web/src/app/home/video-player/video-player.component.ts b/apps/web/src/app/home/video-player/video-player.component.ts index 19d2aaf7b..e58d41192 100644 --- a/apps/web/src/app/home/video-player/video-player.component.ts +++ b/apps/web/src/app/home/video-player/video-player.component.ts @@ -3,7 +3,6 @@ import { ComponentPortal } from '@angular/cdk/portal'; import { AsyncPipe, CommonModule } from '@angular/common'; import { Component, - InjectionToken, Injector, OnInit, effect, @@ -17,6 +16,7 @@ import { StorageMap } from '@ngx-pwa/local-storage'; import { ArtPlayerComponent, AudioPlayerComponent, + COMPONENT_OVERLAY_REF, EpgListComponent, HtmlVideoPlayerComponent, InfoOverlayComponent, @@ -31,7 +31,7 @@ import { selectChannels, selectCurrentEpgProgram, } from 'm3u-state'; -import { Observable, combineLatestWith, filter, map, switchMap } from 'rxjs'; +import { Observable, combineLatest, combineLatestWith, filter, map, switchMap, take } from 'rxjs'; import { DataService, PlaylistsService } from 'services'; import { Channel, @@ -73,10 +73,6 @@ export class VideoPlayerComponent implements OnInit { private readonly storage = inject(StorageMap); private readonly store = inject(Store); - private readonly COMPONENT_OVERLAY_REF = new InjectionToken( - 'COMPONENT_OVERLAY_REF' - ); - /** Active selected channel */ readonly activeChannel$ = this.store .select(selectActive) @@ -126,6 +122,13 @@ export class VideoPlayerComponent implements OnInit { this.applySettings(); this.getPlaylistUrlAsParam(); + // Setup remote control channel change listener (Electron only) + if (this.isDesktop && window.electron?.onChannelChange) { + window.electron.onChannelChange((data: { direction: 'up' | 'down' }) => { + this.handleRemoteChannelChange(data.direction); + }); + } + this.channels$ = this.activatedRoute.params.pipe( combineLatestWith(this.activatedRoute.queryParams), switchMap(([params, queryParams]) => { @@ -162,6 +165,56 @@ export class VideoPlayerComponent implements OnInit { ); } + /** + * Handle remote control channel change + */ + handleRemoteChannelChange(direction: 'up' | 'down'): void { + console.log(`Remote control: changing channel ${direction}`); + + // Use combineLatest to get both values and take only the first emission + combineLatest([this.channels$, this.activeChannel$]) + .pipe( + filter(([channels, activeChannel]) => { + return channels.length > 0 && !!activeChannel; + }), + take(1), + map(([channels, activeChannel]) => { + return { channels, activeChannel: activeChannel as Channel }; + }) + ) + .subscribe({ + next: ({ channels, activeChannel }) => { + // Find current channel index + const currentIndex = channels.findIndex( + ch => ch.url === activeChannel.url + ); + + if (currentIndex === -1) { + console.warn('Current channel not found in channel list'); + return; + } + + // Calculate next/previous index with wraparound + let nextIndex: number; + if (direction === 'up') { + // Up = previous channel (decrease index) + nextIndex = currentIndex - 1 < 0 ? channels.length - 1 : currentIndex - 1; + } else { + // Down = next channel (increase index) + nextIndex = currentIndex + 1 >= channels.length ? 0 : currentIndex + 1; + } + + // Dispatch action to change channel + const nextChannel = channels[nextIndex]; + console.log(`Switching to channel: ${nextChannel.name}`); + this.store.dispatch(PlaylistActions.setActiveChannel({ channel: nextChannel })); + }, + error: (err) => { + console.error('Error changing channel:', err); + } + }); + } + /** * Opens a playlist provided as a url param * e.g. iptvnat.or?url=http://... @@ -214,7 +267,7 @@ export class VideoPlayerComponent implements OnInit { const injector = Injector.create({ providers: [ { - provide: this.COMPONENT_OVERLAY_REF, + provide: COMPONENT_OVERLAY_REF, useValue: this.overlayRef, }, ], diff --git a/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.html b/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.html index 1c0a2e4bc..8414827f7 100644 --- a/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.html +++ b/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.html @@ -42,7 +42,7 @@ " [logo]="channel?.tvg?.logo" (clicked)="selectChannel(channel)" - [selected]="selected?.id === channel?.id" + [selected]="activeChannel()?.url === channel?.url" /> @@ -90,7 +90,7 @@ [logo]="channel?.tvg?.logo" (clicked)="selectChannel(channel)" [selected]=" - selected?.id === channel.id + activeChannel()?.url === channel.url " > } @@ -136,7 +136,7 @@ [isDraggable]="true" [logo]="channel?.tvg?.logo" (clicked)="selectChannel(channel)" - [selected]="selected?.id === channel?.id" + [selected]="activeChannel()?.url === channel?.url" [showFavoriteButton]="true" (favoriteToggled)=" toggleFavoriteChannel(channel, $event) diff --git a/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.ts b/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.ts index 079d9d8cd..2c74f3747 100644 --- a/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.ts +++ b/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.ts @@ -28,7 +28,11 @@ import { Store } from '@ngrx/store'; import { TranslatePipe, TranslateService } from '@ngx-translate/core'; import * as _ from 'lodash'; import * as PlaylistActions from 'm3u-state'; -import { selectActivePlaylistId, selectFavorites } from 'm3u-state'; +import { + selectActive, + selectActivePlaylistId, + selectFavorites, +} from 'm3u-state'; import { map, skipWhile } from 'rxjs'; import { EpgService } from 'services'; import { Channel } from 'shared-interfaces'; @@ -81,7 +85,7 @@ export class ChannelListContainerComponent implements OnDestroy { groupedChannels!: { [key: string]: Channel[] }; /** Selected channel */ - selected!: Channel; + readonly activeChannel = this.store.selectSignal(selectActive); /** Search term for channel filter */ searchTerm: { name: string } = { @@ -123,11 +127,10 @@ export class ChannelListContainerComponent implements OnDestroy { ); /** - * Sets clicked channel as selected and emits them to the parent component + * Sets clicked channel as active and dispatches to store * @param channel selected channel */ selectChannel(channel: Channel): void { - this.selected = channel; this.store.dispatch(PlaylistActions.setActiveChannel({ channel })); // Use tvg-id for EPG matching, fallback to channel name if not available