mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 18:36:15 -08:00
feat: update EPG item description layout and increase dialog width for better visibility
Entire-Checkpoint: 56d3e3141f61
This commit is contained in:
1 parent
cc11ddc9f9
commit
b1a873edaa
6 files changed
+47
-179
No files matched your search
+1
-1
@@ -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 },
|
||||
});
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user