mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
fix: preserve favorites after auto-refresh
This commit is contained in:
1 parent
592f6e34f9
commit
34ef9c57d1
5 files changed
+206
-45
No files matched your search
@@ -151,11 +151,13 @@ ipcMain.handle(AUTO_UPDATE_PLAYLISTS, async (event, playlists) => {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Preserve the original _id and autoRefresh setting
|
||||
// Preserve user data when updating playlist
|
||||
updatedPlaylists.push({
|
||||
...playlistObject,
|
||||
_id: playlist._id,
|
||||
autoRefresh: playlist.autoRefresh,
|
||||
favorites: playlist.favorites || [], // Preserve favorites
|
||||
userAgent: playlist.userAgent, // Preserve custom user agent
|
||||
});
|
||||
|
||||
console.log(`Successfully updated playlist "${playlist.title}"`);
|
||||
|
||||
@@ -3,10 +3,12 @@ import { ComponentPortal } from '@angular/cdk/portal';
|
||||
import { AsyncPipe, CommonModule } from '@angular/common';
|
||||
import {
|
||||
Component,
|
||||
HostListener,
|
||||
Injector,
|
||||
OnInit,
|
||||
effect,
|
||||
inject,
|
||||
viewChild,
|
||||
} from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatSidenavModule } from '@angular/material/sidenav';
|
||||
@@ -31,7 +33,15 @@ import {
|
||||
selectChannels,
|
||||
selectCurrentEpgProgram,
|
||||
} from 'm3u-state';
|
||||
import { Observable, combineLatest, combineLatestWith, filter, map, switchMap, take } from 'rxjs';
|
||||
import {
|
||||
Observable,
|
||||
combineLatest,
|
||||
combineLatestWith,
|
||||
filter,
|
||||
map,
|
||||
switchMap,
|
||||
take,
|
||||
} from 'rxjs';
|
||||
import { DataService, PlaylistsService } from 'services';
|
||||
import {
|
||||
Channel,
|
||||
@@ -97,6 +107,14 @@ export class VideoPlayerComponent implements OnInit {
|
||||
/** EPG overlay reference */
|
||||
private overlayRef!: OverlayRef;
|
||||
|
||||
/** Info overlay component reference for manual triggering */
|
||||
readonly infoOverlay = viewChild(InfoOverlayComponent);
|
||||
|
||||
/** Channel number input state */
|
||||
channelNumberInput = '';
|
||||
showChannelNumberOverlay = false;
|
||||
private channelNumberTimeout?: number;
|
||||
|
||||
volume = 1;
|
||||
|
||||
constructor() {
|
||||
@@ -124,9 +142,11 @@ export class VideoPlayerComponent implements OnInit {
|
||||
|
||||
// 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);
|
||||
});
|
||||
window.electron.onChannelChange(
|
||||
(data: { direction: 'up' | 'down' }) => {
|
||||
this.handleRemoteChannelChange(data.direction);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
this.channels$ = this.activatedRoute.params.pipe(
|
||||
@@ -153,6 +173,26 @@ export class VideoPlayerComponent implements OnInit {
|
||||
channels: playlist.playlist.items,
|
||||
})
|
||||
);
|
||||
|
||||
// Load favorites from the playlist
|
||||
if (
|
||||
playlist.favorites &&
|
||||
playlist.favorites.length > 0
|
||||
) {
|
||||
this.store.dispatch(
|
||||
PlaylistActions.setFavorites({
|
||||
channelIds: playlist.favorites,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
// Clear favorites if playlist has none
|
||||
this.store.dispatch(
|
||||
PlaylistActions.setFavorites({
|
||||
channelIds: [],
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return playlist.playlist.items as Channel[];
|
||||
})
|
||||
);
|
||||
@@ -179,18 +219,20 @@ export class VideoPlayerComponent implements OnInit {
|
||||
}),
|
||||
take(1),
|
||||
map(([channels, activeChannel]) => {
|
||||
return { channels, activeChannel: activeChannel as Channel };
|
||||
return {
|
||||
channels,
|
||||
activeChannel: activeChannel as Channel,
|
||||
};
|
||||
})
|
||||
)
|
||||
.subscribe({
|
||||
next: ({ channels, activeChannel }) => {
|
||||
// Find current channel index
|
||||
const currentIndex = channels.findIndex(
|
||||
ch => ch.url === activeChannel.url
|
||||
(ch) => ch.url === activeChannel.url
|
||||
);
|
||||
|
||||
if (currentIndex === -1) {
|
||||
console.warn('Current channel not found in channel list');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -198,20 +240,29 @@ export class VideoPlayerComponent implements OnInit {
|
||||
let nextIndex: number;
|
||||
if (direction === 'up') {
|
||||
// Up = previous channel (decrease index)
|
||||
nextIndex = currentIndex - 1 < 0 ? channels.length - 1 : currentIndex - 1;
|
||||
nextIndex =
|
||||
currentIndex - 1 < 0
|
||||
? channels.length - 1
|
||||
: currentIndex - 1;
|
||||
} else {
|
||||
// Down = next channel (increase index)
|
||||
nextIndex = currentIndex + 1 >= channels.length ? 0 : currentIndex + 1;
|
||||
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 }));
|
||||
this.store.dispatch(
|
||||
PlaylistActions.setActiveChannel({
|
||||
channel: nextChannel,
|
||||
})
|
||||
);
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('Error changing channel:', err);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -304,4 +355,95 @@ export class VideoPlayerComponent implements OnInit {
|
||||
data: { isDialog: true },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Keyboard shortcut: Press 'I' to toggle EPG info overlay
|
||||
*/
|
||||
@HostListener('document:keydown.i', ['$event'])
|
||||
handleInfoKeyPress(event: Event): void {
|
||||
// Prevent default behavior and show info overlay
|
||||
event.preventDefault();
|
||||
this.toggleInfoOverlay();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle digit key presses for channel number input
|
||||
*/
|
||||
@HostListener('document:keydown', ['$event'])
|
||||
handleKeyPress(event: KeyboardEvent): void {
|
||||
// Only handle digit keys (0-9)
|
||||
if (event.key >= '0' && event.key <= '9') {
|
||||
event.preventDefault();
|
||||
this.handleChannelNumberInput(event.key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the EPG info overlay visibility
|
||||
* Called by 'I' keyboard shortcut or info button
|
||||
*/
|
||||
toggleInfoOverlay(): void {
|
||||
const overlay = this.infoOverlay();
|
||||
if (overlay) {
|
||||
overlay.showOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle channel number input from keyboard
|
||||
* Debounces input to allow multi-digit channel numbers
|
||||
*/
|
||||
handleChannelNumberInput(digit: string): void {
|
||||
// Clear existing timeout
|
||||
if (this.channelNumberTimeout) {
|
||||
clearTimeout(this.channelNumberTimeout);
|
||||
}
|
||||
|
||||
// Add digit to current input
|
||||
this.channelNumberInput += digit;
|
||||
this.showChannelNumberOverlay = true;
|
||||
|
||||
// Set timeout to switch channel after 2 seconds of no input
|
||||
this.channelNumberTimeout = window.setTimeout(() => {
|
||||
this.switchToChannelByNumber(parseInt(this.channelNumberInput, 10));
|
||||
this.clearChannelNumberInput();
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch to channel by number (1-based index)
|
||||
*/
|
||||
switchToChannelByNumber(channelNumber: number): void {
|
||||
this.channels$
|
||||
.pipe(
|
||||
take(1),
|
||||
map((channels) => {
|
||||
// Channel numbers are 1-based, array is 0-based
|
||||
const channelIndex = channelNumber - 1;
|
||||
if (channelIndex >= 0 && channelIndex < channels.length) {
|
||||
return channels[channelIndex];
|
||||
}
|
||||
return null;
|
||||
})
|
||||
)
|
||||
.subscribe((channel) => {
|
||||
if (channel) {
|
||||
this.store.dispatch(
|
||||
PlaylistActions.setActiveChannel({ channel })
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear channel number input and hide overlay
|
||||
*/
|
||||
clearChannelNumberInput(): void {
|
||||
this.channelNumberInput = '';
|
||||
this.showChannelNumberOverlay = false;
|
||||
if (this.channelNumberTimeout) {
|
||||
clearTimeout(this.channelNumberTimeout);
|
||||
this.channelNumberTimeout = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { TranslateService } from '@ngx-translate/core';
|
||||
import {
|
||||
combineLatestWith,
|
||||
EMPTY,
|
||||
filter,
|
||||
firstValueFrom,
|
||||
map,
|
||||
switchMap,
|
||||
@@ -130,6 +131,8 @@ export class PlaylistEffects {
|
||||
setActiveChannel$ = createEffect(() => {
|
||||
return this.actions$.pipe(
|
||||
ofType(PlaylistActions.setActiveChannel),
|
||||
// Skip the effect entirely when channel is falsy
|
||||
filter((action) => !!action.channel),
|
||||
map((action) => {
|
||||
const { channel } = action;
|
||||
|
||||
|
||||
+25
-22
@@ -74,33 +74,36 @@
|
||||
</mat-expansion-panel-header>
|
||||
|
||||
<ng-template matExpansionPanelContent>
|
||||
@for (
|
||||
channel of groups.value;
|
||||
track trackByFn($index, channel);
|
||||
let i = $index
|
||||
) {
|
||||
<app-channel-list-item
|
||||
[name]="
|
||||
i +
|
||||
1 +
|
||||
'. ' +
|
||||
(channel?.name ||
|
||||
'CHANNELS.UNNAMED_CHANNEL'
|
||||
| translate)
|
||||
"
|
||||
[logo]="channel?.tvg?.logo"
|
||||
(clicked)="selectChannel(channel)"
|
||||
[selected]="
|
||||
activeChannel()?.url === channel.url
|
||||
"
|
||||
></app-channel-list-item>
|
||||
}
|
||||
<div class="group-channels">
|
||||
@for (
|
||||
channel of groups.value;
|
||||
track trackByFn($index, channel);
|
||||
let i = $index
|
||||
) {
|
||||
<app-channel-list-item
|
||||
[name]="
|
||||
i +
|
||||
1 +
|
||||
'. ' +
|
||||
(channel?.name ||
|
||||
'CHANNELS.UNNAMED_CHANNEL'
|
||||
| translate)
|
||||
"
|
||||
[logo]="channel?.tvg?.logo"
|
||||
[epgProgram]="getEpgForChannel(channel)"
|
||||
(clicked)="selectChannel(channel)"
|
||||
[selected]="
|
||||
activeChannel()?.url === channel.url
|
||||
"
|
||||
></app-channel-list-item>
|
||||
}
|
||||
</div>
|
||||
</ng-template>
|
||||
</mat-expansion-panel>
|
||||
}
|
||||
}
|
||||
</mat-accordion>
|
||||
</mat-nav-list>
|
||||
</div>
|
||||
</mat-tab>
|
||||
|
||||
@let showFavorites = (playlistId$ | async) !== 'GLOBAL_FAVORITES';
|
||||
|
||||
+21
-10
@@ -33,7 +33,7 @@ import {
|
||||
selectActivePlaylistId,
|
||||
selectFavorites,
|
||||
} from 'm3u-state';
|
||||
import { map, skipWhile } from 'rxjs';
|
||||
import { BehaviorSubject, combineLatest, map, skipWhile } from 'rxjs';
|
||||
import { EpgService } from 'services';
|
||||
import { Channel, EpgProgram } from 'shared-interfaces';
|
||||
import { ChannelListItemComponent } from './channel-list-item/channel-list-item.component';
|
||||
@@ -76,6 +76,8 @@ export class ChannelListContainerComponent implements OnInit, OnDestroy {
|
||||
* Create local copy of the store for local manipulations without updates in the store
|
||||
*/
|
||||
_channelList: Channel[] = [];
|
||||
private channelList$ = new BehaviorSubject<Channel[]>([]);
|
||||
|
||||
get channelList(): Channel[] {
|
||||
return this._channelList;
|
||||
}
|
||||
@@ -83,6 +85,7 @@ export class ChannelListContainerComponent implements OnInit, OnDestroy {
|
||||
@Input()
|
||||
set channelList(value: Channel[]) {
|
||||
this._channelList = value;
|
||||
this.channelList$.next(value); // Emit to observable
|
||||
this.groupedChannels = _.default.groupBy(value, 'group.title');
|
||||
// Fetch EPG for new channel list
|
||||
this.fetchEpgForChannels(value);
|
||||
@@ -119,18 +122,23 @@ export class ChannelListContainerComponent implements OnInit, OnDestroy {
|
||||
)
|
||||
);
|
||||
|
||||
/** List with favorites */
|
||||
favorites$ = this.store.select(selectFavorites).pipe(
|
||||
map(
|
||||
(
|
||||
favoriteChannelIds // TODO: move to selector
|
||||
) =>
|
||||
favoriteChannelIds.map((favoriteChannelId) =>
|
||||
this.channelList.find(
|
||||
/** List with favorites - combines favorites from store with current channel list */
|
||||
favorites$ = combineLatest([
|
||||
this.store.select(selectFavorites),
|
||||
this.channelList$,
|
||||
]).pipe(
|
||||
map(([favoriteChannelIds, channelList]) => {
|
||||
console.log('[ChannelList] favorites$ emit - IDs:', favoriteChannelIds.length, 'Channels:', channelList.length);
|
||||
const favorites = favoriteChannelIds
|
||||
.map((favoriteChannelId) =>
|
||||
channelList.find(
|
||||
(channel) => channel.url === favoriteChannelId
|
||||
)
|
||||
)
|
||||
)
|
||||
.filter((channel): channel is Channel => channel !== undefined); // Filter out undefined channels
|
||||
console.log('[ChannelList] favorites$ result:', favorites.length, 'favorites');
|
||||
return favorites;
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -189,6 +197,9 @@ export class ChannelListContainerComponent implements OnInit, OnDestroy {
|
||||
if (this.epgRefreshInterval) {
|
||||
clearInterval(this.epgRefreshInterval);
|
||||
}
|
||||
|
||||
// Clean up BehaviorSubject
|
||||
this.channelList$.complete();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user