mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-14 13:16:16 -08:00
Takes Angular Material off the renderer's startup path: renderer.initialBytes 1,596,201 -> 1,344,067 bytes (-252,134, 15.8%) at the branch point; merged with master, where #1928 lands the form-field defaults per component, the branch measures 1,345,807 (-188,797, 12.3% against master's baseline). The bundler keeps each module in one chunk, so one eager import of any export carries the module's whole code; each item removes the last eager importer of one Material module. - EPG progress and update panels: eager host + `@defer (when visible)` view with an `@error` notice; the startup spinner is static markup copied from mat-spinner. - LazyMatDialog: the confirm dialog service, player service, parental lock prompt and shell actions resolve MatDialog on first use. - The theme emits tokens for the 21 imported components only, through a `$material-components` list and meta.apply; tools/nx/check-material-theme-scope.mjs (CI) keeps it in step with the imports in both directions. - No provideAnimations(): Material 22 animates with CSS and reads prefers-reduced-motion itself; apps/electron-backend-e2e/src/material-motion.e2e.ts proves dialogs, selects, menus, snack-bars and tooltips animate in and out and stop under reduced motion. Its renderer had deferred every DOM removal to the end of the tick, so the catalog list now tracks the grid offset from scroll events and the player settings panel decides "focus was inside" in a microtask, both with specs that detach the view first. - The snack-bar stays, and with it button, the icon-button chunk and the CDK overlay (about 190 KB): root services inject it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
230 lines
7.0 KiB
TypeScript
230 lines
7.0 KiB
TypeScript
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: `
|
|
<h2 mat-dialog-title>{{ data.title }}</h2>
|
|
<mat-dialog-content class="mat-typography">
|
|
{{ data.message }}
|
|
</mat-dialog-content>
|
|
<mat-dialog-actions align="end">
|
|
<button mat-button mat-dialog-close cdkFocusInitial>
|
|
{{ 'CANCEL' | translate }}
|
|
</button>
|
|
<button mat-flat-button color="primary" [mat-dialog-close]="true">
|
|
{{ data.confirmLabel }}
|
|
</button>
|
|
</mat-dialog-actions>
|
|
`,
|
|
})
|
|
class EpgTrustConfirmDialogComponent {
|
|
readonly data = inject<EpgTrustConfirmDialogData>(MAT_DIALOG_DATA);
|
|
}
|
|
|
|
/**
|
|
* The import progress panel itself. `EpgProgressPanelComponent` defers it
|
|
* until the first import starts, so this file and its Material dependencies
|
|
* (tooltip, progress bar, icon, dialog) must stay out of every file that
|
|
* loads with the app.
|
|
*/
|
|
@Component({
|
|
selector: 'app-epg-progress-panel-view',
|
|
imports: [
|
|
MatButtonModule,
|
|
MatDialogModule,
|
|
MatIconModule,
|
|
MatTooltip,
|
|
MatProgressBar,
|
|
TranslatePipe,
|
|
],
|
|
templateUrl: './epg-progress-panel-view.component.html',
|
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
styleUrl: './epg-progress-panel-view.component.scss',
|
|
})
|
|
export class EpgProgressPanelViewComponent {
|
|
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.translate.instant('EPG.ALLOW_PRIVATE_SOURCE');
|
|
}
|
|
|
|
return this.translate.instant('EPG.TRUST_TLS_HOST');
|
|
}
|
|
|
|
confirmTrust(item: EpgImportProgress): void {
|
|
if (
|
|
item.errorCode ===
|
|
ELECTRON_BRIDGE_SECURITY_ERROR_CODES.EpgPrivateNetworkBlocked
|
|
) {
|
|
this.openTrustDialog(
|
|
{
|
|
title: this.translate.instant(
|
|
'EPG.ALLOW_PRIVATE_SOURCE_TITLE'
|
|
),
|
|
message: this.translate.instant(
|
|
'EPG.ALLOW_PRIVATE_SOURCE_WARNING'
|
|
),
|
|
confirmLabel: this.translate.instant(
|
|
'EPG.ALLOW_PRIVATE_SOURCE'
|
|
),
|
|
},
|
|
() => {
|
|
void this.epgProgress.trustPrivateNetworkSourceAndRetry(
|
|
item.url
|
|
);
|
|
}
|
|
);
|
|
return;
|
|
}
|
|
|
|
this.openTrustDialog(
|
|
{
|
|
title: this.translate.instant('EPG.TRUST_TLS_HOST_TITLE'),
|
|
message: this.translate.instant('EPG.TRUST_TLS_HOST_WARNING'),
|
|
confirmLabel: this.translate.instant('EPG.TRUST_TLS_HOST'),
|
|
},
|
|
() => {
|
|
void this.epgProgress.trustInsecureTlsHostAndRetry(
|
|
item.url,
|
|
item.errorHost
|
|
);
|
|
}
|
|
);
|
|
}
|
|
|
|
private openTrustDialog(
|
|
data: EpgTrustConfirmDialogData,
|
|
onConfirm: () => void
|
|
): void {
|
|
this.dialog
|
|
.open<EpgTrustConfirmDialogComponent, EpgTrustConfirmDialogData>(
|
|
EpgTrustConfirmDialogComponent,
|
|
{ data, width: '420px' }
|
|
)
|
|
.afterClosed()
|
|
.subscribe((confirmed) => {
|
|
if (confirmed) {
|
|
onConfirm();
|
|
}
|
|
});
|
|
}
|
|
}
|