feat: update EPG item description layout and increase dialog width for better visibility

Entire-Checkpoint: 56d3e3141f61
This commit is contained in:
4gray committed 2026-03-26 19:26:01 +01:00
1 parent cc11ddc9f9
commit b1a873edaa
6 files changed
+47 -179

No files matched your search

@@ -13,7 +13,7 @@ $border-subtle: rgba(255, 255, 255, 0.08);
background: linear-gradient(180deg, $bg-surface 0%, $bg-deep 100%);
color: $text-primary;
min-width: 380px;
max-width: 480px;
max-width: 800px;
border-radius: 16px;
overflow: hidden;
position: relative;
@@ -31,6 +31,7 @@ export class EpgListItemComponent {
*/
showDescription(program: EpgProgram): void {
this.dialog.open(EpgItemDescriptionComponent, {
width: '800px',
data: program,
});
}
@@ -3,34 +3,6 @@
<!-- channel info panel -->
<div id="channel-header" color="primary">
<div class="channel-info">
<div class="channel-icon-shell" aria-hidden="true">
@if (showChannelIconFallback(channel)) {
<mat-icon class="channel-icon-fallback">live_tv</mat-icon>
}
@if (getChannelIcon(channel)) {
<img
[src]="getChannelIcon(channel)"
width="48"
alt="icon"
(error)="onChannelIconError($event)"
/>
}
</div>
<div class="channel-details">
<div class="channel-name">
{{ getChannelDisplayName(channel) }}
</div>
<div class="program-name">
{{
playingNow
? (playingNow.title || 'EPG.LIVE_STREAM' | translate)
: ('EPG.LIVE_STREAM' | translate)
}}
</div>
</div>
</div>
<div class="header-divider"></div>
<div id="date-navigator">
<button
class="previous-day"
@@ -76,12 +48,17 @@
<div class="program-title">
{{ program?.title }}
</div>
<div class="epg-item__description">
{{ program?.desc }}
</div>
@if (isProgramPlaying(program)) {
<div class="program-progress">
<div class="epg-progress-track">
<div
class="epg-progress-fill"
[style.width.%]="calculateProgress(program)"
[style.width.%]="
calculateProgress(program)
"
></div>
</div>
</div>
@@ -119,7 +96,9 @@
<div class="epg-progress-track">
<div
class="epg-progress-fill"
[style.width.%]="calculateProgress(program)"
[style.width.%]="
calculateProgress(program)
"
></div>
</div>
</div>
@@ -8,91 +8,6 @@
background: transparent;
}
.header-divider {
height: 1px;
background: rgba(255, 255, 255, 0.08);
}
#channel-header {
display: flex;
flex-direction: column;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.04), rgba(255, 255, 255, 0)),
rgba(127, 127, 127, 0.05);
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
.channel-info {
display: flex;
align-items: center;
padding: 14px 16px 12px;
gap: 14px;
}
}
.channel-icon-shell {
position: relative;
width: 52px;
height: 52px;
flex-shrink: 0;
border-radius: 12px;
overflow: hidden;
display: grid;
place-items: center;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.12), rgba(255, 255, 255, 0)),
rgba(127, 127, 127, 0.12);
box-shadow:
inset 0 0 0 1px rgba(255, 255, 255, 0.08),
inset 0 1px 1px rgba(0, 0, 0, 0.15);
img {
position: relative;
z-index: 1;
width: 100%;
height: 100%;
object-fit: contain;
padding: 6px;
box-sizing: border-box;
}
}
.channel-icon-fallback {
position: absolute;
inset: 0;
margin: auto;
width: 20px;
height: 20px;
font-size: 20px;
opacity: 0.46;
}
.channel-details {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 3px;
}
.channel-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 1rem;
line-height: 1.2;
font-weight: 600;
letter-spacing: 0.01em;
}
.program-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 0.84rem;
line-height: 1.2;
opacity: 0.82;
}
#date-navigator {
display: flex;
justify-content: space-between;
@@ -206,6 +121,16 @@
-webkit-box-orient: vertical;
}
.epg-item__description {
font-size: 0.82rem;
line-height: 1.4;
opacity: 0.74;
display: -webkit-box;
overflow: hidden;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.program-progress {
padding-top: 2px;
}
@@ -3,8 +3,8 @@ import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
effect,
ElementRef,
inject,
Injector,
OnInit,
@@ -14,14 +14,11 @@ import { FormsModule } from '@angular/forms';
import { MatIconButton } from '@angular/material/button';
import { MatIcon } from '@angular/material/icon';
import { MatTooltip } from '@angular/material/tooltip';
import { EpgService } from '@iptvnator/epg/data-access';
import { MomentDatePipe } from '@iptvnator/pipes';
import { Store } from '@ngrx/store';
import { TranslatePipe } from '@ngx-translate/core';
import { EpgService } from '@iptvnator/epg/data-access';
import {
selectActive,
EpgActions,
} from 'm3u-state';
import { EpgActions, selectActive } from 'm3u-state';
import moment from 'moment';
import { BehaviorSubject, combineLatest, map } from 'rxjs';
import { EpgChannel, EpgProgram } from 'shared-interfaces';
@@ -73,8 +70,6 @@ export class EpgListComponent implements OnInit {
/** EPG selected program */
playingNow: EpgProgram | undefined;
channelIconFailed = false;
/** Selected date */
selectedDate!: string;
@@ -96,7 +91,6 @@ export class EpgListComponent implements OnInit {
url: active?.url ? [active.url] : [],
icon: icons,
};
this.channelIconFailed = false;
return (
active?.tvg?.rec || active?.timeshift || active?.catchup?.days
);
@@ -125,45 +119,6 @@ export class EpgListComponent implements OnInit {
)
);
/**
* Helper function to get channel display name
*/
getChannelDisplayName(channel: EpgChannel): string {
if (
!channel ||
!channel.displayName ||
channel.displayName.length === 0
) {
return '';
}
// Return first available display name
return channel.displayName[0]?.value || '';
}
/**
* Helper function to get channel icon
*/
getChannelIcon(channel: EpgChannel): string {
if (!channel || !channel.icon || channel.icon.length === 0) {
return '';
}
// Return first available icon src
return channel.icon[0]?.src || '';
}
showChannelIconFallback(channel: EpgChannel): boolean {
return !this.getChannelIcon(channel) || this.channelIconFailed;
}
onChannelIconError(event: Event): void {
this.channelIconFailed = true;
(event.target as HTMLImageElement | null)?.style.setProperty(
'display',
'none'
);
this.cdr.markForCheck();
}
/**
* Subscribe for values from the store on component init
*/
@@ -54,12 +54,12 @@ interface ProgramSearchResult extends EpgProgram {
@Component({
imports: [
MatButtonModule,
MatIcon,
MatTooltip,
MomentDatePipe,
TranslatePipe
],
MatButtonModule,
MatIcon,
MatTooltip,
MomentDatePipe,
TranslatePipe,
],
selector: 'app-multi-epg-container',
templateUrl: './multi-epg-container.component.html',
styleUrls: ['./multi-epg-container.component.scss'],
@@ -117,7 +117,7 @@ export class MultiEpgContainerComponent
return allChannels;
}
return allChannels.filter(channel => {
return allChannels.filter((channel) => {
const name = this.getChannelName(channel).toLowerCase();
return name.includes(filter);
});
@@ -151,7 +151,7 @@ export class MultiEpgContainerComponent
this.interval = setInterval(() => {
// Force recomputation by updating hourWidth to same value
// This triggers the computed signal to recalculate
this.hourWidth.update(v => v);
this.hourWidth.update((v) => v);
}, 60000);
}
@@ -317,7 +317,10 @@ export class MultiEpgContainerComponent
if (typeof channel.displayName === 'string') {
return channel.displayName;
}
if (Array.isArray(channel.displayName) && channel.displayName.length > 0) {
if (
Array.isArray(channel.displayName) &&
channel.displayName.length > 0
) {
return channel.displayName[0].value;
}
return '';
@@ -338,16 +341,16 @@ export class MultiEpgContainerComponent
zoomIn(): void {
if (this.hourWidth() >= 800) return;
this.hourWidth.update(v => v + 50);
this.hourWidth.update((v) => v + 50);
}
zoomOut(): void {
if (this.hourWidth() <= 50) return;
this.hourWidth.update(v => v - 50);
this.hourWidth.update((v) => v - 50);
}
toggleSearch(): void {
this.isSearchExpanded.update(v => !v);
this.isSearchExpanded.update((v) => !v);
if (!this.isSearchExpanded()) {
this.channelFilter.set('');
}
@@ -373,13 +376,14 @@ export class MultiEpgContainerComponent
showDescription(program: EpgProgram): void {
this.dialog.open(EpgItemDescriptionComponent, {
width: '600px',
data: program,
});
}
// Program search methods
toggleProgramSearch(): void {
this.isProgramSearchOpen.update(v => !v);
this.isProgramSearchOpen.update((v) => !v);
if (!this.isProgramSearchOpen()) {
this.programSearchQuery.set('');
this.programSearchResults.set([]);
@@ -406,7 +410,10 @@ export class MultiEpgContainerComponent
this.searchDebounceTimer = setTimeout(async () => {
this.isSearchingPrograms.set(true);
try {
const results = await window.electron.searchEpgPrograms(query, 20);
const results = await window.electron.searchEpgPrograms(
query,
20
);
this.programSearchResults.set(
(results as ProgramSearchResult[]) || []
);
@@ -429,11 +436,12 @@ export class MultiEpgContainerComponent
const channelId = program.channel_id || program.channel;
// Find channel name
const channel = this.channels().find(ch => ch.id === channelId);
const channel = this.channels().find((ch) => ch.id === channelId);
const channelName = channel ? this.getChannelName(channel) : null;
// Open the program details dialog with channel name
this.dialog.open(EpgItemDescriptionComponent, {
width: '800px',
data: { ...program, channelName },
});
}