import {
Component,
computed,
inject,
signal,
ChangeDetectionStrategy,
} from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import {
MAT_DIALOG_DATA,
MatDialog,
MatDialogModule,
} from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressBar } from '@angular/material/progress-bar';
import { MatTooltip } from '@angular/material/tooltip';
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
import {
EpgImportProgress,
EpgProgressService,
} from '@iptvnator/epg/data-access';
import { ELECTRON_BRIDGE_SECURITY_ERROR_CODES } from '@iptvnator/shared/interfaces';
import { normalizeDateLocale } from '@iptvnator/pipes/date-format';
import { formatEpgImportDisplayUrl } from './epg-import-display-url';
interface EpgTrustConfirmDialogData {
confirmLabel: string;
message: string;
title: string;
}
@Component({
selector: 'app-epg-trust-confirm-dialog',
imports: [MatButtonModule, MatDialogModule, TranslatePipe],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
{{ data.title }}
{{ data.message }}
`,
})
class EpgTrustConfirmDialogComponent {
readonly data = inject(MAT_DIALOG_DATA);
}
@Component({
selector: 'app-epg-progress-panel',
standalone: true,
imports: [
MatButtonModule,
MatDialogModule,
MatIconModule,
MatTooltip,
MatProgressBar,
TranslatePipe,
],
templateUrl: './epg-progress-panel.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrl: './epg-progress-panel.component.scss',
})
export class EpgProgressPanelComponent {
private readonly epgProgress = inject(EpgProgressService);
private readonly dialog = inject(MatDialog);
private readonly translate = inject(TranslateService);
readonly imports = this.epgProgress.imports;
readonly isVisible = this.epgProgress.isVisible;
readonly queuedCount = this.epgProgress.queuedCount;
readonly activeCount = this.epgProgress.activeCount;
readonly minimized = signal(false);
readonly loadingCount = computed(
() => this.imports().filter((i) => i.status === 'loading').length
);
toggleMinimize(): void {
this.minimized.update((v) => !v);
}
get activeImports() {
return this.imports().filter((item) => item.status !== 'queued');
}
get queuedImports() {
return this.imports()
.filter((item) => item.status === 'queued')
.sort((a, b) => (a.queuePosition ?? 0) - (b.queuePosition ?? 0));
}
getStatusIcon(status: EpgImportProgress['status']): string {
switch (status) {
case 'queued':
return 'schedule';
case 'loading':
return 'sync';
case 'complete':
return 'check_circle';
case 'error':
return 'error';
case 'cancelled':
return 'cancel';
}
}
getStatusClass(status: EpgImportProgress['status']): string {
return `status-${status}`;
}
getDisplayUrl(url: string): string {
return formatEpgImportDisplayUrl(url);
}
formatCount(value: number): string {
// App language codes are not all BCP 47 tags (`zhtw`, `ary`, `by`);
// the date pipes already map those aliases for Intl.
const locale = normalizeDateLocale(
this.translate.currentLang || this.translate.defaultLang
);
try {
return new Intl.NumberFormat(locale).format(value);
} catch {
return String(value);
}
}
dismiss(url: string): void {
this.epgProgress.dismiss(url);
}
dismissAll(): void {
this.epgProgress.dismissAll();
}
retry(url: string): void {
this.epgProgress.retry(url);
}
isActionableSecurityError(item: EpgImportProgress): boolean {
return (
item.errorCode ===
ELECTRON_BRIDGE_SECURITY_ERROR_CODES.EpgPrivateNetworkBlocked ||
item.errorCode ===
ELECTRON_BRIDGE_SECURITY_ERROR_CODES.InvalidTlsCertificate
);
}
getActionLabel(item: EpgImportProgress): string {
if (
item.errorCode ===
ELECTRON_BRIDGE_SECURITY_ERROR_CODES.EpgPrivateNetworkBlocked
) {
return this.translateWithFallback(
'EPG.ALLOW_PRIVATE_SOURCE',
'Allow source'
);
}
return this.translateWithFallback('EPG.TRUST_TLS_HOST', 'Trust host');
}
confirmTrust(item: EpgImportProgress): void {
if (
item.errorCode ===
ELECTRON_BRIDGE_SECURITY_ERROR_CODES.EpgPrivateNetworkBlocked
) {
this.openTrustDialog(
{
title: this.translateWithFallback(
'EPG.ALLOW_PRIVATE_SOURCE_TITLE',
'Allow private-network EPG source?'
),
message: this.translateWithFallback(
'EPG.ALLOW_PRIVATE_SOURCE_WARNING',
'Only allow this if you trust the EPG source. IPTVnator will let this exact EPG URL connect to private or local network addresses.'
),
confirmLabel: this.translateWithFallback(
'EPG.ALLOW_PRIVATE_SOURCE',
'Allow source'
),
},
() => {
void this.epgProgress.trustPrivateNetworkSourceAndRetry(
item.url
);
}
);
return;
}
this.openTrustDialog(
{
title: this.translateWithFallback(
'EPG.TRUST_TLS_HOST_TITLE',
'Trust invalid certificate?'
),
message: this.translateWithFallback(
'EPG.TRUST_TLS_HOST_WARNING',
'Only continue if you trust this host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates.'
),
confirmLabel: this.translateWithFallback(
'EPG.TRUST_TLS_HOST',
'Trust host'
),
},
() => {
void this.epgProgress.trustInsecureTlsHostAndRetry(
item.url,
item.errorHost
);
}
);
}
private openTrustDialog(
data: EpgTrustConfirmDialogData,
onConfirm: () => void
): void {
this.dialog
.open(
EpgTrustConfirmDialogComponent,
{ data, width: '420px' }
)
.afterClosed()
.subscribe((confirmed) => {
if (confirmed) {
onConfirm();
}
});
}
private translateWithFallback(key: string, fallback: string): string {
const translated = this.translate.instant(key);
return translated === key ? fallback : translated;
}
}