mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
feat(m3u): add compact mode and conditional EPG display
Add compact layout support for channel list items and make EPG display conditional based on runtime environment and stored settings. - Introduce .compact styles in channel-list-item.component.scss to reduce item height, padding, and logo size when EPG is hidden. - Add StorageMap injection and read STORE_KEY.Settings in ChannelListContainerComponent to determine whether EPG is configured (only enable EPG in Electron when epgUrl exists; disable in PWA). - Expose shouldShowEpg flag and itemSize getter to switch virtual scroll item size between 68 and 48 pixels. - Bind [class.compact] on channel list item and wire [showEpg] input to channel-list-item components so item rendering adapts to mode. - Replace hardcoded itemSize in cdk-virtual-scroll-viewport with dynamic [itemSize] to match compact/expanded layouts. This reduces visual clutter and avoids showing EPG in environments (where it is not configured or supported), improving UX and resource usage.
This commit is contained in:
1 parent
5cf96d7112
commit
9073bf1bb3
4 files changed
+80
-18
No files matched your search
+5
-2
@@ -23,7 +23,7 @@
|
||||
</div>
|
||||
<div id="all-channels" class="channel-list">
|
||||
<cdk-virtual-scroll-viewport
|
||||
itemSize="68"
|
||||
[itemSize]="itemSize"
|
||||
class="scroll-viewport"
|
||||
>
|
||||
<app-channel-list-item
|
||||
@@ -41,6 +41,7 @@
|
||||
| translate)
|
||||
"
|
||||
[logo]="channel?.tvg?.logo"
|
||||
[showEpg]="shouldShowEpg"
|
||||
[epgProgram]="getEpgForChannel(channel)"
|
||||
(clicked)="selectChannel(channel)"
|
||||
[selected]="activeChannel()?.url === channel?.url"
|
||||
@@ -90,10 +91,11 @@
|
||||
| translate)
|
||||
"
|
||||
[logo]="channel?.tvg?.logo"
|
||||
[showEpg]="shouldShowEpg"
|
||||
[epgProgram]="getEpgForChannel(channel)"
|
||||
(clicked)="selectChannel(channel)"
|
||||
[selected]="
|
||||
activeChannel()?.url === channel.url
|
||||
activeChannel()?.url === channel?.url
|
||||
"
|
||||
></app-channel-list-item>
|
||||
}
|
||||
@@ -140,6 +142,7 @@
|
||||
"
|
||||
[isDraggable]="true"
|
||||
[logo]="channel?.tvg?.logo"
|
||||
[showEpg]="shouldShowEpg"
|
||||
[epgProgram]="getEpgForChannel(channel)"
|
||||
(clicked)="selectChannel(channel)"
|
||||
[selected]="activeChannel()?.url === channel?.url"
|
||||
|
||||
+60
-14
@@ -25,6 +25,7 @@ import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { MatTabsModule } from '@angular/material/tabs';
|
||||
import { FilterPipe } from '@iptvnator/pipes';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { StorageMap } from '@ngx-pwa/local-storage';
|
||||
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
|
||||
import * as _ from 'lodash';
|
||||
import * as PlaylistActions from 'm3u-state';
|
||||
@@ -33,9 +34,16 @@ import {
|
||||
selectActivePlaylistId,
|
||||
selectFavorites,
|
||||
} from 'm3u-state';
|
||||
import { BehaviorSubject, combineLatest, map, skipWhile } from 'rxjs';
|
||||
import {
|
||||
BehaviorSubject,
|
||||
combineLatest,
|
||||
map,
|
||||
skipWhile,
|
||||
Subject,
|
||||
takeUntil,
|
||||
} from 'rxjs';
|
||||
import { EpgService } from 'services';
|
||||
import { Channel, EpgProgram } from 'shared-interfaces';
|
||||
import { Channel, EpgProgram, Settings, STORE_KEY } from 'shared-interfaces';
|
||||
import { ChannelListItemComponent } from './channel-list-item/channel-list-item.component';
|
||||
|
||||
@Component({
|
||||
@@ -62,6 +70,7 @@ import { ChannelListItemComponent } from './channel-list-item/channel-list-item.
|
||||
export class ChannelListContainerComponent implements OnInit, OnDestroy {
|
||||
private readonly epgService = inject(EpgService);
|
||||
private readonly snackBar = inject(MatSnackBar);
|
||||
private readonly storage = inject(StorageMap);
|
||||
private readonly store = inject(Store);
|
||||
private readonly translateService = inject(TranslateService);
|
||||
|
||||
@@ -71,6 +80,16 @@ export class ChannelListContainerComponent implements OnInit, OnDestroy {
|
||||
/** Interval for refreshing EPG data */
|
||||
private epgRefreshInterval?: number;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
/** Whether to show EPG data in channel items (false in PWA mode or when EPG is not configured) */
|
||||
shouldShowEpg = false;
|
||||
|
||||
/** Item size for virtual scroll - compact when no EPG */
|
||||
get itemSize(): number {
|
||||
return this.shouldShowEpg ? 68 : 48;
|
||||
}
|
||||
|
||||
/**
|
||||
* Channels array
|
||||
* Create local copy of the store for local manipulations without updates in the store
|
||||
@@ -128,7 +147,12 @@ export class ChannelListContainerComponent implements OnInit, OnDestroy {
|
||||
this.channelList$,
|
||||
]).pipe(
|
||||
map(([favoriteChannelIds, channelList]) => {
|
||||
console.log('[ChannelList] favorites$ emit - IDs:', favoriteChannelIds.length, 'Channels:', channelList.length);
|
||||
console.log(
|
||||
'[ChannelList] favorites$ emit - IDs:',
|
||||
favoriteChannelIds.length,
|
||||
'Channels:',
|
||||
channelList.length
|
||||
);
|
||||
const favorites = favoriteChannelIds
|
||||
.map((favoriteChannelId) =>
|
||||
channelList.find(
|
||||
@@ -136,7 +160,11 @@ export class ChannelListContainerComponent implements OnInit, OnDestroy {
|
||||
)
|
||||
)
|
||||
.filter((channel): channel is Channel => channel !== undefined); // Filter out undefined channels
|
||||
console.log('[ChannelList] favorites$ result:', favorites.length, 'favorites');
|
||||
console.log(
|
||||
'[ChannelList] favorites$ result:',
|
||||
favorites.length,
|
||||
'favorites'
|
||||
);
|
||||
return favorites;
|
||||
})
|
||||
);
|
||||
@@ -185,6 +213,25 @@ export class ChannelListContainerComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
// Check if EPG should be shown (only in Electron with configured EPG URL)
|
||||
const isElectron = !!window['electron'];
|
||||
if (isElectron) {
|
||||
this.storage
|
||||
.get(STORE_KEY.Settings)
|
||||
.subscribe((settings: unknown) => {
|
||||
if (
|
||||
settings &&
|
||||
Object.keys(settings as Settings).length > 0
|
||||
) {
|
||||
const epgUrl = (settings as Settings).epgUrl;
|
||||
this.shouldShowEpg = !!(epgUrl && epgUrl.length > 0);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// PWA mode - don't show EPG
|
||||
this.shouldShowEpg = false;
|
||||
}
|
||||
|
||||
// Set up EPG refresh interval (every 60 seconds)
|
||||
this.epgRefreshInterval = window.setInterval(() => {
|
||||
this.fetchEpgForChannels(this._channelList);
|
||||
@@ -198,6 +245,8 @@ export class ChannelListContainerComponent implements OnInit, OnDestroy {
|
||||
clearInterval(this.epgRefreshInterval);
|
||||
}
|
||||
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
// Clean up BehaviorSubject
|
||||
this.channelList$.complete();
|
||||
}
|
||||
@@ -213,19 +262,16 @@ export class ChannelListContainerComponent implements OnInit, OnDestroy {
|
||||
|
||||
// Get channel IDs (prefer tvg-id, fallback to name)
|
||||
const channelIds = channels
|
||||
.map(channel => channel?.tvg?.id?.trim() || channel?.name?.trim())
|
||||
.filter(id => !!id);
|
||||
|
||||
console.log(`[EPG] Fetching EPG for ${channelIds.length} channels`);
|
||||
.map((channel) => channel?.tvg?.id?.trim() || channel?.name?.trim())
|
||||
.filter((id) => !!id);
|
||||
|
||||
// Batch fetch EPG programs
|
||||
this.epgService.getCurrentProgramsForChannels(channelIds).subscribe(
|
||||
(epgMap) => {
|
||||
console.log(`[EPG] Received EPG data for ${epgMap.size} channels`);
|
||||
console.log('[EPG] Sample data:', Array.from(epgMap.entries()).slice(0, 3));
|
||||
this.epgService
|
||||
.getCurrentProgramsForChannels(channelIds)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((epgMap) => {
|
||||
this.channelEpgMap = epgMap;
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+14
-2
@@ -19,6 +19,12 @@
|
||||
&.active {
|
||||
background-color: rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
// Compact mode when EPG is not shown
|
||||
&.compact {
|
||||
height: 48px;
|
||||
padding: 6px 12px;
|
||||
}
|
||||
}
|
||||
|
||||
// Dark theme support
|
||||
@@ -57,6 +63,12 @@
|
||||
border-radius: 4px;
|
||||
object-fit: contain;
|
||||
flex-shrink: 0;
|
||||
|
||||
// Smaller logo in compact mode
|
||||
.compact & {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
// Channel details (name + EPG)
|
||||
@@ -83,9 +95,9 @@
|
||||
|
||||
// EPG title (program name)
|
||||
.epg-title {
|
||||
font-family: 'Crimson Pro', serif;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
font-weight: 500;
|
||||
opacity: 0.85;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
+1
@@ -26,6 +26,7 @@ import { EpgProgram } from 'shared-interfaces';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `<div
|
||||
class="channel-list-item"
|
||||
[class.compact]="!showEpg"
|
||||
cdkDrag
|
||||
[cdkDragDisabled]="!isDraggable"
|
||||
cdkDragPreviewContainer="parent"
|
||||
|
||||
Reference in new issue
Block a user