style: enhance channel details dialog with improved layout and additional stats

Entire-Checkpoint: db35a98d337f
This commit is contained in:
4gray committed 2026-04-11 10:42:10 +02:00
1 parent c97673b928
commit dd58e4a529
4 files changed
+671 -280

No files matched your search

@@ -1,109 +1,233 @@
<h2 mat-dialog-title>{{ 'CHANNELS.DETAILS_DIALOG.TITLE' | translate }}</h2>
<section mat-dialog-title class="channel-dialog__hero">
<div class="channel-dialog__hero-copy">
<div class="channel-dialog__eyebrow">
<span class="channel-dialog__eyebrow-mark"></span>
{{ channel.group?.title || 'M3U' }}
</div>
<mat-dialog-content class="channel-details-dialog mat-typography">
<p class="dialog-description">
{{ 'CHANNELS.DETAILS_DIALOG.DESCRIPTION' | translate }}
</p>
<div class="channel-dialog__headline-row">
<div class="channel-dialog__headline-text">
@if (channel.tvg?.logo) {
<img
class="channel-dialog__logo"
[src]="channel.tvg.logo"
[alt]="channel.name"
(error)="logoError = true"
[hidden]="logoError"
/>
}
<div>
<h2 class="channel-dialog__title">{{ channel.name }}</h2>
<p class="channel-dialog__subtitle">
{{ 'CHANNELS.DETAILS_DIALOG.DESCRIPTION' | translate }}
</p>
</div>
</div>
<section class="details-section">
<h3 class="section-title">
{{ 'CHANNELS.DETAILS_DIALOG.SUMMARY' | translate }}
</h3>
<dl class="details-grid">
@for (field of summaryFields; track field.labelKey) {
<div class="detail-row">
<dt>{{ field.labelKey | translate }}</dt>
<dd
[class.empty]="field.empty"
[class.monospace]="field.monospace"
[class.wrap]="field.wrap"
<div class="channel-dialog__status-cluster">
@if (channel.radio === 'true') {
<span
class="channel-dialog__status-pill channel-dialog__status-pill--radio"
>
@if (field.valueKey) {
{{ field.valueKey | translate: field.translateParams }}
} @else {
{{ field.value }}
{{ 'CHANNELS.DETAILS_DIALOG.RADIO' | translate }}
</span>
}
@if (catchupAvailable) {
<span
class="channel-dialog__status-pill channel-dialog__status-pill--active"
>
{{ 'CHANNELS.DETAILS_DIALOG.CATCHUP_AVAILABLE' | translate }}
</span>
}
@if (catchupPlaybackSupported) {
<span
class="channel-dialog__status-pill channel-dialog__status-pill--playback"
>
{{ 'CHANNELS.DETAILS_DIALOG.PLAYBACK_SUPPORTED' | translate }}
</span>
}
</div>
</div>
</div>
<div class="channel-dialog__hero-stats">
@for (stat of heroStats; track stat.labelKey) {
<article class="channel-stat">
<div class="channel-stat__icon">
<mat-icon>{{ stat.icon }}</mat-icon>
</div>
<div class="channel-stat__label">
{{ stat.labelKey | translate }}
</div>
<div
class="channel-stat__value"
[class.channel-stat__value--mono]="stat.mono"
[class.channel-stat__value--empty]="stat.empty"
>
@if (stat.valueKey) {
{{ stat.valueKey | translate: stat.translateParams }}
} @else {
{{ stat.value }}
}
</div>
</article>
}
</div>
</section>
<mat-dialog-content class="channel-dialog__content">
<div class="channel-dialog__grid">
<section class="channel-panel">
<div class="channel-panel__header">
<p class="channel-panel__eyebrow">
{{ 'CHANNELS.DETAILS_DIALOG.SUMMARY' | translate }}
</p>
<h3>{{ 'CHANNELS.DETAILS_DIALOG.STREAM_URL' | translate }}</h3>
</div>
<div class="channel-dialog__url-block">
<code>{{ channel.url }}</code>
<button
mat-icon-button
class="channel-dialog__copy-btn"
[cdkCopyToClipboard]="channel.url"
>
<mat-icon>content_copy</mat-icon>
</button>
</div>
<dl class="channel-detail-list">
@for (field of streamFields; track field.labelKey) {
<div class="channel-detail-row">
<dt>{{ field.labelKey | translate }}</dt>
<dd
[class.channel-detail-row__value--mono]="
field.monospace
"
[class.channel-detail-row__value--empty]="
field.empty
"
>
@if (field.valueKey) {
{{
field.valueKey
| translate: field.translateParams
}}
} @else {
{{ field.value }}
}
</dd>
</div>
}
</dl>
@if (hasTvgData) {
<div class="channel-chip-section">
<p class="channel-chip-section__label">
{{ 'CHANNELS.DETAILS_DIALOG.TVG' | translate }}
</p>
<dl class="channel-detail-list">
@for (field of tvgFields; track field.labelKey) {
<div class="channel-detail-row">
<dt>{{ field.labelKey | translate }}</dt>
<dd
[class.channel-detail-row__value--mono]="
field.monospace
"
[class.channel-detail-row__value--empty]="
field.empty
"
>
@if (field.valueKey) {
{{
field.valueKey
| translate
: field.translateParams
}}
} @else {
{{ field.value }}
}
</dd>
</div>
}
</dd>
</dl>
</div>
}
</dl>
</section>
</section>
<section class="details-section">
<h3 class="section-title">
{{ 'CHANNELS.DETAILS_DIALOG.TVG' | translate }}
</h3>
<dl class="details-grid">
@for (field of tvgFields; track field.labelKey) {
<div class="detail-row">
<dt>{{ field.labelKey | translate }}</dt>
<dd
[class.empty]="field.empty"
[class.monospace]="field.monospace"
[class.wrap]="field.wrap"
>
@if (field.valueKey) {
{{ field.valueKey | translate: field.translateParams }}
} @else {
{{ field.value }}
<section class="channel-panel">
<div class="channel-panel__header">
<p class="channel-panel__eyebrow">
{{ 'CHANNELS.DETAILS_DIALOG.CATCHUP' | translate }}
</p>
<h3>
{{ channel.catchup?.type || ('CHANNELS.DETAILS_DIALOG.NOT_AVAILABLE' | translate) }}
</h3>
</div>
<dl class="channel-detail-list">
@for (field of catchupFields; track field.labelKey) {
<div class="channel-detail-row">
<dt>{{ field.labelKey | translate }}</dt>
<dd
[class.channel-detail-row__value--mono]="
field.monospace
"
[class.channel-detail-row__value--empty]="
field.empty
"
>
@if (field.valueKey) {
{{
field.valueKey
| translate: field.translateParams
}}
} @else {
{{ field.value }}
}
</dd>
</div>
}
</dl>
@if (hasHttpData) {
<div class="channel-chip-section">
<p class="channel-chip-section__label">
{{ 'CHANNELS.DETAILS_DIALOG.HTTP' | translate }}
</p>
<dl class="channel-detail-list">
@for (field of httpFields; track field.labelKey) {
<div class="channel-detail-row">
<dt>{{ field.labelKey | translate }}</dt>
<dd
[class.channel-detail-row__value--mono]="
field.monospace
"
[class.channel-detail-row__value--empty]="
field.empty
"
>
@if (field.valueKey) {
{{
field.valueKey
| translate
: field.translateParams
}}
} @else {
{{ field.value }}
}
</dd>
</div>
}
</dd>
</dl>
</div>
}
</dl>
</section>
<section class="details-section">
<h3 class="section-title">
{{ 'CHANNELS.DETAILS_DIALOG.CATCHUP' | translate }}
</h3>
<dl class="details-grid">
@for (field of catchupFields; track field.labelKey) {
<div class="detail-row">
<dt>{{ field.labelKey | translate }}</dt>
<dd
[class.empty]="field.empty"
[class.monospace]="field.monospace"
[class.wrap]="field.wrap"
>
@if (field.valueKey) {
{{ field.valueKey | translate: field.translateParams }}
} @else {
{{ field.value }}
}
</dd>
</div>
}
</dl>
</section>
<section class="details-section">
<h3 class="section-title">
{{ 'CHANNELS.DETAILS_DIALOG.HTTP' | translate }}
</h3>
<dl class="details-grid">
@for (field of httpFields; track field.labelKey) {
<div class="detail-row">
<dt>{{ field.labelKey | translate }}</dt>
<dd
[class.empty]="field.empty"
[class.monospace]="field.monospace"
[class.wrap]="field.wrap"
>
@if (field.valueKey) {
{{ field.valueKey | translate: field.translateParams }}
} @else {
{{ field.value }}
}
</dd>
</div>
}
</dl>
</section>
</section>
</div>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button mat-dialog-close>
<mat-dialog-actions class="channel-dialog__actions">
<button mat-stroked-button mat-dialog-close class="channel-dialog__close">
{{ 'CLOSE' | translate }}
</button>
</mat-dialog-actions>
@@ -1,80 +1,390 @@
:host {
display: block;
color: var(--app-body-color, var(--mat-sys-on-surface));
}
.channel-details-dialog {
max-height: min(78vh, 720px);
}
.dialog-description {
margin: 0 0 20px;
color: var(--mat-sys-on-surface-variant);
}
.details-section + .details-section {
margin-top: 24px;
padding-top: 20px;
border-top: 1px solid var(--mat-sys-outline-variant);
}
.section-title {
margin: 0 0 12px;
font-size: 0.92rem;
font-weight: 600;
line-height: 1.3;
}
.details-grid {
// ─── Hero header ────────────────────────────────────────────────────────────
.channel-dialog__hero {
margin: 0;
padding: 20px 20px 18px;
display: grid;
gap: 12px;
margin: 0;
gap: 18px;
background:
var(--app-accent-gradient),
var(--app-widget-header-bg, var(--mat-sys-surface-container-high));
border-bottom: 1px solid var(--app-widget-header-border, rgba(255, 255, 255, 0.05));
}
.detail-row {
.channel-dialog__hero-copy {
display: grid;
grid-template-columns: minmax(140px, 180px) minmax(0, 1fr);
gap: 6px 16px;
align-items: start;
gap: 10px;
}
dt {
.channel-dialog__eyebrow {
display: inline-flex;
align-items: center;
gap: 8px;
margin: 0;
color: var(--mat-sys-on-surface-variant);
color: var(--app-eyebrow-color, var(--mat-sys-on-surface-variant));
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.1em;
text-transform: uppercase;
}
dd {
margin: 0;
.channel-dialog__eyebrow-mark {
width: 8px;
height: 8px;
border-radius: 999px;
background: var(--mat-sys-primary);
box-shadow: 0 0 0 4px color-mix(in srgb, var(--mat-sys-primary) 14%, transparent);
}
.channel-dialog__headline-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 14px;
}
.channel-dialog__headline-text {
display: flex;
align-items: center;
gap: 14px;
min-width: 0;
line-height: 1.45;
}
&.empty {
color: var(--mat-sys-on-surface-variant);
.channel-dialog__logo {
width: 48px;
height: 48px;
border-radius: 12px;
object-fit: contain;
background: var(--mat-sys-surface-container);
border: 1px solid var(--app-separator);
flex-shrink: 0;
}
.channel-dialog__title {
margin: 0;
font-size: 1.3rem;
line-height: 1.2;
font-weight: 650;
letter-spacing: -0.02em;
color: var(--app-heading-color, var(--mat-sys-on-surface));
}
.channel-dialog__subtitle {
margin: 4px 0 0;
color: var(--app-muted-color, var(--mat-sys-on-surface-variant));
font-size: 0.88rem;
}
// ─── Status pills ───────────────────────────────────────────────────────────
.channel-dialog__status-cluster {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 6px;
flex-shrink: 0;
}
.channel-dialog__status-pill {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 26px;
padding: 0 10px;
border-radius: 999px;
border: 1px solid var(--app-separator);
background: var(--mat-sys-surface-container);
color: var(--app-body-color, var(--mat-sys-on-surface));
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.channel-dialog__status-pill--active {
border-color: color-mix(in srgb, #34d399 30%, transparent);
background: color-mix(in srgb, #34d399 12%, transparent);
color: #2db36f;
}
.channel-dialog__status-pill--radio {
border-color: color-mix(in srgb, var(--mat-sys-tertiary) 30%, transparent);
background: color-mix(in srgb, var(--mat-sys-tertiary) 12%, transparent);
color: var(--mat-sys-tertiary);
}
.channel-dialog__status-pill--playback {
border-color: color-mix(in srgb, var(--mat-sys-primary) 30%, transparent);
background: color-mix(in srgb, var(--mat-sys-primary) 12%, transparent);
color: var(--mat-sys-primary);
}
// ─── Hero stat cards ────────────────────────────────────────────────────────
.channel-dialog__hero-stats {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
}
.channel-stat {
display: grid;
gap: 4px;
padding: 12px 14px;
border-radius: 12px;
border: 1px solid var(--app-widget-border, transparent);
background: var(--app-widget-bg, var(--mat-sys-surface-container));
box-shadow: var(--app-widget-shadow, none);
outline: 1px solid rgba(255, 255, 255, 0.04);
outline-offset: -1px;
}
.channel-stat__icon {
width: 32px;
height: 32px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 10px;
background: color-mix(in srgb, var(--mat-sys-primary) 12%, transparent);
color: var(--mat-sys-primary);
mat-icon {
font-size: 18px;
width: 18px;
height: 18px;
}
}
.channel-stat__label {
color: var(--app-muted-color, var(--mat-sys-on-surface-variant));
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.channel-stat__value {
font-size: 1.1rem;
font-weight: 650;
letter-spacing: -0.02em;
color: var(--app-heading-color, var(--mat-sys-on-surface));
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
&--mono {
font-family: 'JetBrains Mono', 'Roboto Mono', monospace;
font-size: 0.92rem;
}
&--empty {
color: var(--app-muted-color, var(--mat-sys-on-surface-variant));
font-style: italic;
font-weight: 500;
font-size: 0.88rem;
}
}
&.monospace {
font-family:
ui-monospace,
SFMono-Regular,
Menlo,
Monaco,
Consolas,
'Liberation Mono',
'Courier New',
monospace;
font-size: 0.92em;
}
// ─── Content area ───────────────────────────────────────────────────────────
.channel-dialog__content {
margin: 0;
padding: 20px;
background: var(--app-widget-bg, var(--mat-sys-surface));
}
&.wrap {
.channel-dialog__grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
}
// ─── URL block ──────────────────────────────────────────────────────────────
.channel-dialog__url-block {
display: flex;
align-items: flex-start;
gap: 8px;
padding: 10px 10px 10px 14px;
border-radius: 10px;
border: 1px solid var(--app-separator);
background: var(--mat-sys-surface-container);
code {
flex: 1;
min-width: 0;
font-family: 'JetBrains Mono', 'Roboto Mono', monospace;
font-size: 0.82rem;
line-height: 1.5;
color: var(--app-body-color, var(--mat-sys-on-surface));
overflow-wrap: anywhere;
word-break: break-word;
}
}
@media (max-width: 640px) {
.detail-row {
grid-template-columns: minmax(0, 1fr);
.channel-dialog__copy-btn {
flex-shrink: 0;
width: 32px;
height: 32px;
--mdc-icon-button-state-layer-size: 32px;
--mdc-icon-button-icon-size: 18px;
color: var(--app-muted-color, var(--mat-sys-on-surface-variant));
}
// ─── Detail panels ──────────────────────────────────────────────────────────
.channel-panel {
display: grid;
gap: 14px;
padding: 16px;
border-radius: 16px;
border: 1px solid var(--app-widget-border, transparent);
background: var(--app-widget-bg, var(--mat-sys-surface-container-low));
box-shadow: var(--app-widget-shadow, none);
outline: 1px solid rgba(255, 255, 255, 0.04);
outline-offset: -1px;
align-content: start;
}
.channel-panel__header {
display: grid;
gap: 4px;
}
.channel-panel__header h3 {
margin: 0;
font-size: 1rem;
font-weight: 600;
letter-spacing: -0.01em;
color: var(--app-heading-color, var(--mat-sys-on-surface));
}
.channel-panel__eyebrow {
margin: 0;
color: var(--app-eyebrow-color, var(--mat-sys-on-surface-variant));
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.1em;
text-transform: uppercase;
}
// ─── Detail rows ────────────────────────────────────────────────────────────
.channel-detail-list {
display: grid;
gap: 0;
margin: 0;
}
.channel-detail-row {
display: grid;
grid-template-columns: minmax(100px, 130px) minmax(0, 1fr);
gap: 12px;
align-items: baseline;
padding: 8px 0;
border-bottom: 1px solid var(--app-separator);
}
.channel-detail-row:last-child {
border-bottom: 0;
padding-bottom: 0;
}
.channel-detail-row dt {
margin: 0;
color: var(--app-muted-color, var(--mat-sys-on-surface-variant));
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.channel-detail-row dd {
margin: 0;
color: var(--app-body-color, var(--mat-sys-on-surface));
font-size: 0.88rem;
font-weight: 500;
word-break: break-word;
overflow-wrap: anywhere;
}
.channel-detail-row__value--mono {
font-family: 'JetBrains Mono', 'Roboto Mono', monospace;
font-size: 0.82rem;
}
.channel-detail-row__value--empty {
color: var(--app-muted-color, var(--mat-sys-on-surface-variant));
font-style: italic;
}
// ─── Chip sections (sub-group headers) ──────────────────────────────────────
.channel-chip-section {
display: grid;
gap: 10px;
padding-top: 4px;
}
.channel-chip-section__label {
margin: 0;
color: var(--app-eyebrow-color, var(--mat-sys-on-surface-variant));
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.1em;
text-transform: uppercase;
padding-top: 4px;
border-top: 1px solid var(--app-separator);
}
// ─── Footer actions ─────────────────────────────────────────────────────────
.channel-dialog__actions {
padding: 0 20px 20px;
display: flex;
justify-content: flex-end;
gap: 10px;
border-top: 1px solid var(--app-widget-header-border, rgba(255, 255, 255, 0.05));
background: var(--app-widget-bg, var(--mat-sys-surface));
}
.channel-dialog__close {
min-width: 100px;
border-radius: 999px;
}
// ─── Responsive breakpoints ─────────────────────────────────────────────────
@media (max-width: 720px) {
.channel-dialog__hero,
.channel-dialog__content,
.channel-dialog__actions {
padding-left: 16px;
padding-right: 16px;
}
.channel-dialog__headline-row {
flex-direction: column;
align-items: flex-start;
}
.channel-dialog__status-cluster {
justify-content: flex-start;
}
.channel-dialog__hero-stats,
.channel-dialog__grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.channel-detail-row {
grid-template-columns: 1fr;
gap: 4px;
}
}
@media (max-width: 480px) {
.channel-dialog__hero-stats {
grid-template-columns: 1fr;
}
.channel-dialog__grid {
grid-template-columns: 1fr;
}
}
@@ -62,23 +62,13 @@ describe('ChannelDetailsDialogComponent', () => {
expect(component.archiveDays).toBe(5);
expect(component.catchupAvailable).toBe(true);
expect(component.catchupPlaybackSupported).toBe(true);
expect(component.summaryFields).toEqual(
expect(component.heroStats).toEqual(
expect.arrayContaining([
expect.objectContaining({
labelKey: 'CHANNELS.DETAILS_DIALOG.CATCHUP_AVAILABLE',
valueKey: 'YES',
}),
expect.objectContaining({
labelKey: 'CHANNELS.DETAILS_DIALOG.WINDOW',
translateParams: {
count: 5,
},
translateParams: { count: 5 },
valueKey: 'CHANNELS.DETAILS_DIALOG.DAYS_OTHER',
}),
expect.objectContaining({
labelKey: 'CHANNELS.DETAILS_DIALOG.PLAYBACK_SUPPORTED',
valueKey: 'YES',
}),
])
);
});
@@ -1,6 +1,8 @@
import { ClipboardModule } from '@angular/cdk/clipboard';
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
import { TranslatePipe } from '@ngx-translate/core';
import { getM3uArchiveDays, isM3uCatchupPlaybackSupported } from 'm3u-utils';
import { Channel } from 'shared-interfaces';
@@ -12,7 +14,16 @@ interface ChannelDetailField {
readonly translateParams?: Record<string, number>;
readonly value?: string;
readonly valueKey?: string;
readonly wrap?: boolean;
}
interface HeroStat {
readonly empty?: boolean;
readonly icon: string;
readonly labelKey: string;
readonly mono?: boolean;
readonly translateParams?: Record<string, number>;
readonly value?: string;
readonly valueKey?: string;
}
@Component({
@@ -20,7 +31,7 @@ interface ChannelDetailField {
templateUrl: './channel-details-dialog.component.html',
styleUrls: ['./channel-details-dialog.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [MatButtonModule, MatDialogModule, TranslatePipe],
imports: [ClipboardModule, MatButtonModule, MatDialogModule, MatIconModule, TranslatePipe],
})
export class ChannelDetailsDialogComponent {
readonly channel = inject<Channel>(MAT_DIALOG_DATA);
@@ -31,151 +42,121 @@ export class ChannelDetailsDialogComponent {
this.channel
);
readonly summaryFields: ChannelDetailField[] = [
this.createTextField(
'CHANNELS.DETAILS_DIALOG.NAME',
this.channel.name
),
this.createTextField(
'CHANNELS.DETAILS_DIALOG.CHANNEL_ID',
this.channel.id,
{
monospace: true,
}
),
this.createTextField(
'CHANNELS.DETAILS_DIALOG.STREAM_URL',
this.channel.url,
{
monospace: true,
wrap: true,
}
),
this.createTextField(
'CHANNELS.DETAILS_DIALOG.GROUP',
this.channel.group?.title
),
this.createBooleanField(
'CHANNELS.DETAILS_DIALOG.RADIO',
this.channel.radio === 'true'
),
this.createBooleanField(
'CHANNELS.DETAILS_DIALOG.CATCHUP_AVAILABLE',
this.catchupAvailable
),
this.createArchiveWindowField(),
this.createBooleanField(
'CHANNELS.DETAILS_DIALOG.PLAYBACK_SUPPORTED',
this.catchupPlaybackSupported
),
this.createTextField(
logoError = false;
readonly hasTvgData = !!(
this.channel.tvg?.id ||
this.channel.tvg?.name ||
this.channel.tvg?.url ||
this.channel.tvg?.rec
);
readonly hasHttpData = !!(
this.channel.http?.origin ||
this.channel.http?.referrer ||
this.channel.http?.['user-agent']
);
readonly heroStats: HeroStat[] = [
{
icon: 'tag',
labelKey: 'CHANNELS.DETAILS_DIALOG.CHANNEL_ID',
value: this.channel.id?.trim() || undefined,
valueKey: this.channel.id?.trim()
? undefined
: 'CHANNELS.DETAILS_DIALOG.EMPTY',
mono: true,
empty: !this.channel.id?.trim(),
},
{
icon: 'folder',
labelKey: 'CHANNELS.DETAILS_DIALOG.GROUP',
value: this.channel.group?.title?.trim() || undefined,
valueKey: this.channel.group?.title?.trim()
? undefined
: 'CHANNELS.DETAILS_DIALOG.EMPTY',
empty: !this.channel.group?.title?.trim(),
},
this.createArchiveHeroStat(),
{
icon: 'schedule',
labelKey: 'CHANNELS.DETAILS_DIALOG.TIMESHIFT',
value: this.channel.timeshift?.trim() || undefined,
valueKey: this.channel.timeshift?.trim()
? undefined
: 'CHANNELS.DETAILS_DIALOG.NOT_AVAILABLE',
empty: !this.channel.timeshift?.trim(),
},
];
readonly streamFields: ChannelDetailField[] = [
this.field(
'CHANNELS.DETAILS_DIALOG.EPG_PARAMS',
this.channel.epgParams,
{
monospace: true,
wrap: true,
}
true
),
this.createTextField(
'CHANNELS.DETAILS_DIALOG.TIMESHIFT',
this.channel.timeshift
this.field(
'CHANNELS.DETAILS_DIALOG.TVG_LOGO',
this.channel.tvg?.logo,
true
),
];
readonly tvgFields: ChannelDetailField[] = [
this.createTextField(
'CHANNELS.DETAILS_DIALOG.TVG_ID',
this.channel.tvg?.id,
{
monospace: true,
}
),
this.createTextField(
'CHANNELS.DETAILS_DIALOG.TVG_NAME',
this.channel.tvg?.name
),
this.createTextField(
this.field('CHANNELS.DETAILS_DIALOG.TVG_ID', this.channel.tvg?.id, true),
this.field('CHANNELS.DETAILS_DIALOG.TVG_NAME', this.channel.tvg?.name),
this.field(
'CHANNELS.DETAILS_DIALOG.TVG_URL',
this.channel.tvg?.url,
{
monospace: true,
wrap: true,
}
),
this.createTextField(
'CHANNELS.DETAILS_DIALOG.TVG_LOGO',
this.channel.tvg?.logo,
{
monospace: true,
wrap: true,
}
),
this.createTextField(
'CHANNELS.DETAILS_DIALOG.TVG_REC',
this.channel.tvg?.rec
true
),
this.field('CHANNELS.DETAILS_DIALOG.TVG_REC', this.channel.tvg?.rec),
];
readonly catchupFields: ChannelDetailField[] = [
this.createTextField(
'CHANNELS.DETAILS_DIALOG.CATCHUP_TYPE',
this.channel.catchup?.type
),
this.createTextField(
this.field(
'CHANNELS.DETAILS_DIALOG.CATCHUP_SOURCE',
this.channel.catchup?.source,
{
monospace: true,
wrap: true,
}
true
),
this.createTextField(
this.field(
'CHANNELS.DETAILS_DIALOG.CATCHUP_DAYS',
this.channel.catchup?.days
),
];
readonly httpFields: ChannelDetailField[] = [
this.createTextField(
this.field(
'CHANNELS.DETAILS_DIALOG.HTTP_ORIGIN',
this.channel.http?.origin,
{
monospace: true,
wrap: true,
}
true
),
this.createTextField(
this.field(
'CHANNELS.DETAILS_DIALOG.HTTP_REFERRER',
this.channel.http?.referrer,
{
monospace: true,
wrap: true,
}
true
),
this.createTextField(
this.field(
'CHANNELS.DETAILS_DIALOG.HTTP_USER_AGENT',
this.channel.http?.['user-agent'],
{
monospace: true,
wrap: true,
}
true
),
];
private createArchiveWindowField(): ChannelDetailField {
private createArchiveHeroStat(): HeroStat {
if (!this.catchupAvailable) {
return {
empty: true,
icon: 'history',
labelKey: 'CHANNELS.DETAILS_DIALOG.WINDOW',
valueKey: 'CHANNELS.DETAILS_DIALOG.NOT_AVAILABLE',
};
}
return {
icon: 'history',
labelKey: 'CHANNELS.DETAILS_DIALOG.WINDOW',
translateParams: {
count: this.archiveDays,
},
translateParams: { count: this.archiveDays },
valueKey:
this.archiveDays === 1
? 'CHANNELS.DETAILS_DIALOG.DAYS_ONE'
@@ -183,36 +164,22 @@ export class ChannelDetailsDialogComponent {
};
}
private createBooleanField(
labelKey: string,
value: boolean
): ChannelDetailField {
return {
labelKey,
valueKey: value ? 'YES' : 'NO',
};
}
private createTextField(
private field(
labelKey: string,
value: string | null | undefined,
options: Pick<ChannelDetailField, 'monospace' | 'wrap'> = {}
monospace = false
): ChannelDetailField {
const normalized = value?.trim() ?? '';
if (!normalized) {
return {
...options,
empty: true,
labelKey,
monospace,
valueKey: 'CHANNELS.DETAILS_DIALOG.EMPTY',
};
}
return {
...options,
labelKey,
value: normalized,
};
return { labelKey, monospace, value: normalized };
}
}