style(ui/info-overlay): redesign EPG layout and styles

Refactor the info-overlay template to introduce a structured EPG
layout: replace the previous flat channel-logo + content structure with
an .epg-layout wrapper that groups .channel-logo and a richer .content
area. Split content into .content-header (separate program title and
channel name), conditional program description rendering, and a
program-timeline containing explicit start/end times and a progress
track. Remove fixed image width attribute and rely on responsive CSS.

Revamp component SCSS: import refined fonts, increase overlay max-height,
add horizontal padding, and add new styles for .epg-layout, .channel-logo,
.content, .content-header, .program-title and related elements. Add
animations, improved image handling (object-fit + drop shadow), and
layout spacing to support varying content lengths and improve visual
hierarchy.

These changes improve responsiveness, accessibility of EPG text
display, and visual polish of the overlay.
This commit is contained in:
4gray committed 2025-11-22 11:40:16 +01:00
1 parent ce6aa1b3d1
commit 3bf328343b
4 files changed
+214 -125

No files matched your search

+1
View File
@@ -213,6 +213,7 @@
"FETCH_EPG": "Fetching EPG data...",
"ERROR": "Ooops, EPG could not be loaded.",
"DOWNLOAD_SUCCESS": "EPG was successfully downloaded.",
"NO_PROGRAM_INFO": "No program information available",
"PROGRAM_DIALOG": {
"PROGRAM_DETAILS": "Details about the TV program",
"TITLE": "Title",
@@ -3,54 +3,67 @@
class="channel-overlay"
[class.visible]="isVisible"
[class.hidden]="!isVisible"
[class.no-epg]="!epgProgram"
>
@if (epgProgram) {
<!-- Layout with EPG data -->
<div class="channel-logo">
<img
[src]="epgProgram?.icon?.[0]?.src || channel?.tvg?.logo"
width="68"
onerror="this.style.display='none'"
alt="channel icon"
/>
</div>
<div class="content">
<div class="program-name">
{{ epgProgram?.title?.[0]?.value + ' |' }}
{{ channel.name }}
<div class="epg-layout">
<div class="channel-logo">
<img
[src]="epgProgram?.icon?.[0]?.src || channel?.tvg?.logo"
onerror="this.style.display='none'"
alt="channel icon"
/>
</div>
<div class="program-description">
{{ epgProgram?.desc?.[0]?.value }}
</div>
<div class="program-progress">
<div
class="program-finished"
[ngStyle]="{
width: (finishedDuration * 100) / generalDuration + '%',
}"
></div>
<div class="duration">
{{ start | momentDate: 'YYYYMMDDHHmm ZZ' : 'HH:mm' }}
-
{{ stop | momentDate: 'YYYYMMDDHHmm ZZ' : 'HH:mm' }}
<div class="content">
<div class="content-header">
<div class="program-title">
{{ epgProgram?.title?.[0]?.value }}
</div>
<div class="channel-name">
{{ channel.name }}
</div>
</div>
@if (epgProgram?.desc?.[0]?.value) {
<div class="program-description">
{{ epgProgram?.desc?.[0]?.value }}
</div>
}
<div class="program-timeline">
<div class="timeline-info">
<span class="time-start">{{ start | momentDate: 'YYYYMMDDHHmm ZZ' : 'HH:mm' }}</span>
<span class="time-separator">—</span>
<span class="time-end">{{ stop | momentDate: 'YYYYMMDDHHmm ZZ' : 'HH:mm' }}</span>
</div>
<div class="progress-track">
<div
class="progress-fill"
[ngStyle]="{
width: (finishedDuration * 100) / generalDuration + '%',
}"
></div>
</div>
</div>
</div>
</div>
} @else {
<!-- Layout without EPG data -->
<div class="no-epg-layout">
@if (channel?.tvg?.logo) {
<div class="channel-logo-large">
<img
[src]="channel.tvg.logo"
onerror="this.style.display='none'"
alt="channel icon"
/>
<!-- Layout without EPG data - same structure, minimal content -->
<div class="epg-layout">
<div class="channel-logo">
<img
[src]="channel?.tvg?.logo"
onerror="this.style.display='none'"
alt="channel icon"
/>
</div>
<div class="content">
<div class="content-header">
<div class="program-title no-epg-title">
{{ channel.name }}
</div>
<div class="channel-name no-epg-subtitle">
{{ 'EPG.NO_PROGRAM_INFO' | translate }}
</div>
</div>
}
<div class="channel-name-large">
{{ channel.name }}
</div>
</div>
}
@@ -1,17 +1,20 @@
// Import refined fonts
@import url('https://fonts.googleapis.com/css2?family=Crimson+Pro:wght@400;600;700&family=DM+Sans:wght@400;500;600&display=swap');
.channel-overlay {
position: absolute;
bottom: 35px;
width: 100%;
min-height: 130px;
max-height: 165px;
max-height: 200px;
margin-left: auto;
margin-right: auto;
left: 0;
right: 0;
flex-direction: row;
box-sizing: border-box;
display: flex;
isolation: isolate;
padding: 0 40px;
// Gradient blur effect using pseudo-element
&::before {
@@ -25,74 +28,172 @@
z-index: -1;
}
&.no-epg {
min-height: 100px;
max-height: 180px;
display: flex;
align-items: center;
justify-content: center;
}
}
.program-name,
.program-description {
margin: 20px 0;
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
// EPG Layout with data
.epg-layout {
display: flex;
gap: 28px;
width: 100%;
align-items: flex-start;
padding: 24px 0;
animation: fadeInUp 0.5s ease-out;
}
.channel-logo {
margin-top: 20px;
place-content: flex-start center;
align-items: flex-start;
flex-direction: row;
box-sizing: border-box;
flex-shrink: 0;
display: flex;
flex: 1 1 100px;
max-width: 100px;
min-width: 100px;
align-items: center;
justify-content: center;
width: 90px;
height: 90px;
img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.4));
}
}
.content {
width: 100%;
flex: 1;
display: flex;
flex-direction: column;
gap: 12px;
min-width: 0; // Allow text truncation
}
.program-progress {
width: 96%;
background: #ccc;
height: 20px;
margin-bottom: 8px;
.content-header {
display: flex;
flex-direction: column;
gap: 6px;
animation: fadeIn 0.6s ease-out 0.1s backwards;
}
.program-name {
width: 96%;
max-height: 20px;
-webkit-line-clamp: 1;
.program-title {
font-family: 'Crimson Pro', serif;
font-size: 2.25rem;
font-weight: 700;
line-height: 1.2;
color: #ffffff;
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.6);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
letter-spacing: -0.02em;
}
.channel-name {
font-family: 'DM Sans', sans-serif;
font-size: 0.95rem;
font-weight: 500;
color: rgba(255, 255, 255, 0.75);
text-transform: uppercase;
letter-spacing: 0.08em;
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
}
.program-description {
width: 96%;
max-height: 55px;
-webkit-line-clamp: 3;
font-family: 'DM Sans', sans-serif;
font-size: 1.05rem;
line-height: 1.5;
color: rgba(255, 255, 255, 0.88);
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
text-overflow: ellipsis;
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
animation: fadeIn 0.6s ease-out 0.2s backwards;
}
.program-finished {
background: #717171;
height: 20px;
font-size: 0.8em;
padding-left: 10px;
color: #000;
line-height: 20px;
.program-timeline {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 4px;
animation: fadeIn 0.6s ease-out 0.3s backwards;
}
.duration {
.timeline-info {
display: flex;
align-items: center;
gap: 8px;
font-family: 'DM Sans', sans-serif;
font-size: 0.875rem;
font-weight: 500;
color: rgba(255, 255, 255, 0.9);
letter-spacing: 0.02em;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
}
.time-separator {
opacity: 0.5;
margin: 0 2px;
}
.progress-track {
position: relative;
top: -18px;
font-size: 0.8em;
color: #000;
left: 10px;
width: 100%;
height: 4px;
background: rgba(255, 255, 255, 0.15);
border-radius: 2px;
overflow: hidden;
backdrop-filter: blur(4px);
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.3);
}
.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.3s ease-out;
box-shadow: 0 0 12px rgba(124, 58, 237, 0.6);
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.3) 50%, transparent 100%);
animation: shimmer 2s ease-in-out infinite;
}
}
// Animations
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes shimmer {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}
.visible {
@@ -106,41 +207,14 @@
transition: visibility 0s 2s, opacity 2s linear;
}
// No EPG layout styles
.no-epg-layout {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
padding: 20px;
gap: 20px;
// No EPG state variations
.no-epg-title {
// When there's no EPG, the channel name becomes the main title
font-size: 2.5rem; // Slightly larger since it's the only content
}
.channel-logo-large {
display: flex;
align-items: center;
justify-content: center;
img {
max-width: 150px;
max-height: 100px;
width: auto;
height: auto;
object-fit: contain;
}
}
.channel-name-large {
font-size: 2.5rem;
font-weight: 600;
text-align: center;
line-height: 1.2;
max-width: 90%;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.7);
.no-epg-subtitle {
// Subtle message indicating no EPG data
font-style: italic;
opacity: 0.6;
}
@@ -1,11 +1,12 @@
import { NgStyle } from '@angular/common';
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { MomentDatePipe } from '@iptvnator/pipes';
import { TranslatePipe } from '@ngx-translate/core';
import moment from 'moment';
import { Channel, EpgProgram } from 'shared-interfaces';
@Component({
imports: [MomentDatePipe, NgStyle],
imports: [MomentDatePipe, NgStyle, TranslatePipe],
selector: 'app-info-overlay',
templateUrl: './info-overlay.component.html',
styleUrls: ['./info-overlay.component.scss'],
@@ -45,7 +46,7 @@ export class InfoOverlayComponent implements OnChanges {
this.isVisible = true;
this.currentTimeout = setTimeout(() => {
this.isVisible = false;
}, 4000);
}, 10000);
}
if (changes['epgProgram'] && changes['epgProgram'].currentValue) {
const { stop, start } = changes['epgProgram'].currentValue;