Skip to content

Commit 4ff916c

Browse files
committed
feat: use signal for active channel + listen for remote control events
- Replace local selected state with store signal (selectActive) in the channel list container so templates bind to activeChannel()?.url instead of comparing selected?.id. This unifies selection handling with the global store and prevents stale local state. - Update selectChannel to dispatch setActiveChannel instead of mutating local selected property; keep EPG lookup behavior intact. - Import selectActive alongside other selectors from m3u-state and remove unused local InjectionToken in video player. - Add remote control listener in VideoPlayer (Electron) to handle channel up/down events and wire a new handleRemoteChannelChange method skeleton. Use combineLatest/take to safely read required observables for channel switching. - Minor rxjs import and formatting adjustments. These changes centralize active channel state in the store, simplify template logic, and add support for remote channel changes on desktop.
1 parent 463be94 commit 4ff916c

File tree

3 files changed

+70
-14
lines changed

3 files changed

+70
-14
lines changed

apps/web/src/app/home/video-player/video-player.component.ts

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { ComponentPortal } from '@angular/cdk/portal';
33
import { AsyncPipe, CommonModule } from '@angular/common';
44
import {
55
Component,
6-
InjectionToken,
76
Injector,
87
OnInit,
98
effect,
@@ -17,6 +16,7 @@ import { StorageMap } from '@ngx-pwa/local-storage';
1716
import {
1817
ArtPlayerComponent,
1918
AudioPlayerComponent,
19+
COMPONENT_OVERLAY_REF,
2020
EpgListComponent,
2121
HtmlVideoPlayerComponent,
2222
InfoOverlayComponent,
@@ -31,7 +31,7 @@ import {
3131
selectChannels,
3232
selectCurrentEpgProgram,
3333
} from 'm3u-state';
34-
import { Observable, combineLatestWith, filter, map, switchMap } from 'rxjs';
34+
import { Observable, combineLatest, combineLatestWith, filter, map, switchMap, take } from 'rxjs';
3535
import { DataService, PlaylistsService } from 'services';
3636
import {
3737
Channel,
@@ -73,10 +73,6 @@ export class VideoPlayerComponent implements OnInit {
7373
private readonly storage = inject(StorageMap);
7474
private readonly store = inject(Store);
7575

76-
private readonly COMPONENT_OVERLAY_REF = new InjectionToken<OverlayRef>(
77-
'COMPONENT_OVERLAY_REF'
78-
);
79-
8076
/** Active selected channel */
8177
readonly activeChannel$ = this.store
8278
.select(selectActive)
@@ -126,6 +122,13 @@ export class VideoPlayerComponent implements OnInit {
126122
this.applySettings();
127123
this.getPlaylistUrlAsParam();
128124

125+
// Setup remote control channel change listener (Electron only)
126+
if (this.isDesktop && window.electron?.onChannelChange) {
127+
window.electron.onChannelChange((data: { direction: 'up' | 'down' }) => {
128+
this.handleRemoteChannelChange(data.direction);
129+
});
130+
}
131+
129132
this.channels$ = this.activatedRoute.params.pipe(
130133
combineLatestWith(this.activatedRoute.queryParams),
131134
switchMap(([params, queryParams]) => {
@@ -162,6 +165,56 @@ export class VideoPlayerComponent implements OnInit {
162165
);
163166
}
164167

168+
/**
169+
* Handle remote control channel change
170+
*/
171+
handleRemoteChannelChange(direction: 'up' | 'down'): void {
172+
console.log(`Remote control: changing channel ${direction}`);
173+
174+
// Use combineLatest to get both values and take only the first emission
175+
combineLatest([this.channels$, this.activeChannel$])
176+
.pipe(
177+
filter(([channels, activeChannel]) => {
178+
return channels.length > 0 && !!activeChannel;
179+
}),
180+
take(1),
181+
map(([channels, activeChannel]) => {
182+
return { channels, activeChannel: activeChannel as Channel };
183+
})
184+
)
185+
.subscribe({
186+
next: ({ channels, activeChannel }) => {
187+
// Find current channel index
188+
const currentIndex = channels.findIndex(
189+
ch => ch.url === activeChannel.url
190+
);
191+
192+
if (currentIndex === -1) {
193+
console.warn('Current channel not found in channel list');
194+
return;
195+
}
196+
197+
// Calculate next/previous index with wraparound
198+
let nextIndex: number;
199+
if (direction === 'up') {
200+
// Up = previous channel (decrease index)
201+
nextIndex = currentIndex - 1 < 0 ? channels.length - 1 : currentIndex - 1;
202+
} else {
203+
// Down = next channel (increase index)
204+
nextIndex = currentIndex + 1 >= channels.length ? 0 : currentIndex + 1;
205+
}
206+
207+
// Dispatch action to change channel
208+
const nextChannel = channels[nextIndex];
209+
console.log(`Switching to channel: ${nextChannel.name}`);
210+
this.store.dispatch(PlaylistActions.setActiveChannel({ channel: nextChannel }));
211+
},
212+
error: (err) => {
213+
console.error('Error changing channel:', err);
214+
}
215+
});
216+
}
217+
165218
/**
166219
* Opens a playlist provided as a url param
167220
* e.g. iptvnat.or?url=http://...
@@ -214,7 +267,7 @@ export class VideoPlayerComponent implements OnInit {
214267
const injector = Injector.create({
215268
providers: [
216269
{
217-
provide: this.COMPONENT_OVERLAY_REF,
270+
provide: COMPONENT_OVERLAY_REF,
218271
useValue: this.overlayRef,
219272
},
220273
],

libs/ui/components/src/lib/channel-list-container/channel-list-container.component.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
"
4343
[logo]="channel?.tvg?.logo"
4444
(clicked)="selectChannel(channel)"
45-
[selected]="selected?.id === channel?.id"
45+
[selected]="activeChannel()?.url === channel?.url"
4646
/>
4747
</cdk-virtual-scroll-viewport>
4848
</mat-nav-list>
@@ -90,7 +90,7 @@
9090
[logo]="channel?.tvg?.logo"
9191
(clicked)="selectChannel(channel)"
9292
[selected]="
93-
selected?.id === channel.id
93+
activeChannel()?.url === channel.url
9494
"
9595
></app-channel-list-item>
9696
}
@@ -136,7 +136,7 @@
136136
[isDraggable]="true"
137137
[logo]="channel?.tvg?.logo"
138138
(clicked)="selectChannel(channel)"
139-
[selected]="selected?.id === channel?.id"
139+
[selected]="activeChannel()?.url === channel?.url"
140140
[showFavoriteButton]="true"
141141
(favoriteToggled)="
142142
toggleFavoriteChannel(channel, $event)

libs/ui/components/src/lib/channel-list-container/channel-list-container.component.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,11 @@ import { Store } from '@ngrx/store';
2828
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
2929
import * as _ from 'lodash';
3030
import * as PlaylistActions from 'm3u-state';
31-
import { selectActivePlaylistId, selectFavorites } from 'm3u-state';
31+
import {
32+
selectActive,
33+
selectActivePlaylistId,
34+
selectFavorites,
35+
} from 'm3u-state';
3236
import { map, skipWhile } from 'rxjs';
3337
import { EpgService } from 'services';
3438
import { Channel } from 'shared-interfaces';
@@ -81,7 +85,7 @@ export class ChannelListContainerComponent implements OnDestroy {
8185
groupedChannels!: { [key: string]: Channel[] };
8286

8387
/** Selected channel */
84-
selected!: Channel;
88+
readonly activeChannel = this.store.selectSignal(selectActive);
8589

8690
/** Search term for channel filter */
8791
searchTerm: { name: string } = {
@@ -123,11 +127,10 @@ export class ChannelListContainerComponent implements OnDestroy {
123127
);
124128

125129
/**
126-
* Sets clicked channel as selected and emits them to the parent component
130+
* Sets clicked channel as active and dispatches to store
127131
* @param channel selected channel
128132
*/
129133
selectChannel(channel: Channel): void {
130-
this.selected = channel;
131134
this.store.dispatch(PlaylistActions.setActiveChannel({ channel }));
132135

133136
// Use tvg-id for EPG matching, fallback to channel name if not available

0 commit comments

Comments
 (0)