diff --git a/libs/ui/components/src/lib/epg-list/epg-list-item/epg-list-item.component.scss b/libs/ui/components/src/lib/epg-list/epg-list-item/epg-list-item.component.scss
index 62700d796..fb8d24679 100644
--- a/libs/ui/components/src/lib/epg-list/epg-list-item/epg-list-item.component.scss
+++ b/libs/ui/components/src/lib/epg-list/epg-list-item/epg-list-item.component.scss
@@ -1,30 +1,106 @@
+:host {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ width: 100%;
+}
+
.time {
- color: #666;
- border-bottom: 1px solid #ccc;
- font-size: 0.8em;
- display: inline-block;
- margin-bottom: 5px;
- margin-left: 5px;
+ font-family: 'DM Sans', sans-serif;
+ font-size: 0.7rem;
+ opacity: 0.6;
+ letter-spacing: 0.02em;
+ line-height: 1;
+ white-space: nowrap;
}
.badge {
- border-radius: 2px;
- padding: 2px 6px;
- color: #fff;
+ border-radius: 4px;
+ padding: 3px 8px;
+ color: white;
text-transform: uppercase;
- margin-left: 6px;
- font-size: 0.8em;
+ font-size: 10px;
+ font-weight: 600;
+ letter-spacing: 0.08em;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 40px;
+ transition: all 0.2s ease;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.live {
- background-color: red;
+ background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
+ animation: livePulse 2s ease-in-out infinite;
+ box-shadow: 0 0 12px rgba(239, 68, 68, 0.5), 0 2px 8px rgba(0, 0, 0, 0.3);
+
+ &::after {
+ content: '●';
+ margin-left: 4px;
+ animation: blink 1.5s ease-in-out infinite;
+ }
}
.rec {
- background-color: grey;
+ background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
+ box-shadow: 0 0 12px rgba(139, 92, 246, 0.4), 0 2px 8px rgba(0, 0, 0, 0.3);
+
+ &::after {
+ content: '◉';
+ margin-left: 4px;
+ }
}
.description-icon {
- float: right;
+ margin-left: auto;
cursor: pointer;
+ opacity: 0.6;
+ transition: all 0.2s ease;
+ padding: 4px;
+ border-radius: 4px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ mat-icon {
+ font-size: 18px;
+ width: 18px;
+ height: 18px;
+ }
+
+ &:hover {
+ opacity: 1;
+ background: rgba(0, 0, 0, 0.04);
+ }
+
+ &:active {
+ transform: scale(0.95);
+ }
+}
+
+// Dark theme support
+::ng-deep .dark-theme {
+ .description-icon:hover {
+ background: rgba(255, 255, 255, 0.04);
+ }
+}
+
+// Animations
+@keyframes livePulse {
+ 0%, 100% {
+ box-shadow: 0 0 12px rgba(239, 68, 68, 0.5), 0 2px 8px rgba(0, 0, 0, 0.3);
+ }
+ 50% {
+ box-shadow: 0 0 20px rgba(239, 68, 68, 0.8), 0 2px 12px rgba(0, 0, 0, 0.4);
+ }
+}
+
+@keyframes blink {
+ 0%, 100% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0.3;
+ }
}
diff --git a/libs/ui/components/src/lib/epg-list/epg-list.component.html b/libs/ui/components/src/lib/epg-list/epg-list.component.html
index 260047d49..58cfeb869 100644
--- a/libs/ui/components/src/lib/epg-list/epg-list.component.html
+++ b/libs/ui/components/src/lib/epg-list/epg-list.component.html
@@ -23,7 +23,7 @@
-
+
-
@if (timeshiftUntil) {
-
+
@if (items?.length > 0) {
- @for (program of items; track program.start + program.stop) {
+ @for (program of items; track $index) {
@if (
program.start < timeshiftUntil || program.start >= timeNow
) {
-
-
+
+
-
+
{{ program?.title?.[0]?.value }}
-
-
+
+ @if (isProgramPlaying(program)) {
+
+ }
+
} @else {
-
= program.start && timeNow <= program.stop
"
@@ -87,25 +92,34 @@
)
"
>
-
+ @if (isProgramPlaying(program)) {
+
+ }
+
}
-
}
} @else {
-
+
{{ 'EPG.EPG_NOT_AVAILABLE_DATE' | translate }}
-
+
}
-
+
}
diff --git a/libs/ui/components/src/lib/epg-list/epg-list.component.scss b/libs/ui/components/src/lib/epg-list/epg-list.component.scss
index 7841ab869..af1a42077 100644
--- a/libs/ui/components/src/lib/epg-list/epg-list.component.scss
+++ b/libs/ui/components/src/lib/epg-list/epg-list.component.scss
@@ -10,14 +10,20 @@
overflow-x: hidden;
}
-.active {
- border-bottom: 1px dashed #666;
+// Header divider
+.header-divider {
+ height: 1px;
+ background: rgba(0, 0, 0, 0.12);
}
-.program-title {
- font-size: 14px;
+// Dark theme support
+::ng-deep .dark-theme {
+ .header-divider {
+ background: rgba(255, 255, 255, 0.12);
+ }
}
+// Channel header
#channel-header {
flex-direction: column;
box-sizing: border-box;
@@ -27,20 +33,27 @@
flex-direction: row;
box-sizing: border-box;
display: flex;
- padding: 10px;
+ padding: 12px;
+ gap: 12px;
}
.channel-icon {
overflow: hidden;
- margin-right: 10px;
place-content: center;
align-items: center;
flex-direction: row;
box-sizing: border-box;
display: flex;
- flex: 1 1 60px;
- max-width: 60px;
- min-width: 60px;
+ flex: 0 0 48px;
+ width: 48px;
+ height: 48px;
+ border-radius: 4px;
+
+ img {
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+ }
}
.channel-details {
@@ -50,41 +63,153 @@
align-self: center;
display: flex;
flex-direction: column;
+ gap: 2px;
+ min-width: 0;
}
.channel-name {
- font-weight: 800;
+ font-family: 'DM Sans', sans-serif;
+ font-weight: 500;
+ font-size: 0.9rem;
+ line-height: 1.3;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
.program-name {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
+ font-size: 0.8rem;
+ font-weight: 500;
+ opacity: 0.85;
overflow: hidden;
text-overflow: ellipsis;
- display: -webkit-box;
- -webkit-line-clamp: 1;
- -webkit-box-orient: vertical;
- font-size: 0.9em;
+ white-space: nowrap;
+ line-height: 1.2;
}
}
-#program-list {
- flex: 1 1 1e-9px;
- box-sizing: border-box;
-}
-
-.epg-not-available {
- padding: 0 10px;
-}
-
-mat-selection-list {
- overflow: auto;
-}
-
+// Date navigator
#date-navigator {
display: flex;
justify-content: space-between;
align-items: center;
+ padding: 8px;
+ gap: 8px;
+
+ button {
+ transition: all 0.2s ease;
+
+ &[disabled] {
+ opacity: 0.3;
+ }
+ }
.selected-date {
- font-size: 0.9em;
+ flex: 1;
+ font-family: 'DM Sans', sans-serif;
+ font-size: 0.85rem;
+ font-weight: 500;
+ text-align: center;
+ opacity: 0.8;
}
}
+
+// Program list styling
+#program-list {
+ flex: 1;
+ overflow-y: auto;
+ overflow-x: hidden;
+}
+
+// Program list items
+.program-item {
+ border-bottom: 1px solid rgba(0, 0, 0, 0.08);
+ transition: background-color 0.2s ease;
+ position: relative;
+ padding: 10px 12px;
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ cursor: default;
+
+ &.clickable {
+ cursor: pointer;
+
+ &:hover {
+ background-color: rgba(0, 0, 0, 0.04);
+ }
+ }
+
+ &.active {
+ background-color: rgba(0, 0, 0, 0.08);
+ }
+
+ .program-meta {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ }
+
+ .program-title {
+ font-family: 'DM Sans', sans-serif;
+ font-size: 0.9rem;
+ font-weight: 500;
+ line-height: 1.4;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ }
+
+ .program-progress {
+ margin-top: 4px;
+ }
+}
+
+// Dark theme support
+::ng-deep .dark-theme {
+ .program-item {
+ border-bottom-color: rgba(255, 255, 255, 0.08);
+
+ &.clickable:hover {
+ background-color: rgba(255, 255, 255, 0.04);
+ }
+
+ &.active {
+ background-color: rgba(255, 255, 255, 0.08);
+ }
+ }
+}
+
+// Progress bar (matching channel list)
+.epg-progress-track {
+ position: relative;
+ width: 100%;
+ height: 3px;
+ background: rgba(128, 128, 128, 0.2);
+ border-radius: 2px;
+ overflow: hidden;
+}
+
+.epg-progress-fill {
+ position: absolute;
+ left: 0;
+ top: 0;
+ height: 100%;
+ background: linear-gradient(90deg, #4f46e5 0%, #7c3aed 50%, #ec4899 100%);
+ border-radius: 2px;
+ transition: width 0.5s ease-out;
+ box-shadow: 0 0 6px rgba(124, 58, 237, 0.4);
+}
+
+// Empty state styling
+.epg-not-available {
+ padding: 24px;
+ text-align: center;
+ font-family: 'DM Sans', sans-serif;
+ font-size: 0.875rem;
+ opacity: 0.6;
+ font-style: italic;
+}
diff --git a/libs/ui/components/src/lib/epg-list/epg-list.component.ts b/libs/ui/components/src/lib/epg-list/epg-list.component.ts
index 206998108..434d5716f 100644
--- a/libs/ui/components/src/lib/epg-list/epg-list.component.ts
+++ b/libs/ui/components/src/lib/epg-list/epg-list.component.ts
@@ -1,10 +1,8 @@
import { AsyncPipe } from '@angular/common';
-import { Component, inject, OnInit } from '@angular/core';
+import { ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, OnInit } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatIconButton } from '@angular/material/button';
-import { MatDivider } from '@angular/material/divider';
import { MatIcon } from '@angular/material/icon';
-import { MatListModule } from '@angular/material/list';
import { MatTooltip } from '@angular/material/tooltip';
import { MomentDatePipe } from '@iptvnator/pipes';
import { Store } from '@ngrx/store';
@@ -34,10 +32,8 @@ const DATE_FORMAT = 'YYYY-MM-DD';
AsyncPipe,
EpgListItemComponent,
FormsModule,
- MatDivider,
MatIcon,
MatIconButton,
- MatListModule,
MatTooltip,
MomentDatePipe,
TranslatePipe,
@@ -45,10 +41,12 @@ const DATE_FORMAT = 'YYYY-MM-DD';
selector: 'app-epg-list',
templateUrl: './epg-list.component.html',
styleUrls: ['./epg-list.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush,
})
export class EpgListComponent implements OnInit {
private readonly store = inject(Store);
private readonly epgService = inject(EpgService);
+ private readonly cdr = inject(ChangeDetectorRef);
/** Channel info in EPG format */
channel!: EpgChannel;
@@ -169,6 +167,9 @@ export class EpgListComponent implements OnInit {
// Clear the current EPG program when no programs available
this.store.dispatch(resetActiveEpgProgram());
}
+
+ // Trigger change detection for OnPush strategy
+ this.cdr.markForCheck();
}
/**
@@ -224,6 +225,8 @@ export class EpgListComponent implements OnInit {
this.store.dispatch(
setCurrentEpgProgram({ program: this.playingNow })
);
+ // Trigger change detection for OnPush strategy
+ this.cdr.markForCheck();
});
}
@@ -245,5 +248,33 @@ export class EpgListComponent implements OnInit {
this.store.dispatch(setActiveEpgProgram({ program }));
}
this.playingNow = program;
+ // Trigger change detection for OnPush strategy
+ this.cdr.markForCheck();
+ }
+
+ /**
+ * Calculates the progress percentage for the EPG program
+ */
+ calculateProgress(program: EpgProgram): number {
+ const now = new Date().getTime();
+ const start = new Date(program.start).getTime();
+ const stop = new Date(program.stop).getTime();
+
+ const total = stop - start;
+ const elapsed = now - start;
+
+ const progress = Math.min(100, Math.max(0, (elapsed / total) * 100));
+
+ return progress;
+ }
+
+ /**
+ * Check if program is currently playing
+ */
+ isProgramPlaying(program: EpgProgram): boolean {
+ const isPlaying =
+ this.timeNow >= program.start && this.timeNow <= program.stop;
+
+ return isPlaying;
}
}