Files
iptvnator/apps/web/src/app/app.config.ts
T
4grayandClaude Opus 5.5 8d01ae5f75 perf(web): provide form-field defaults per component, not at the root
app.config.ts provided MAT_FORM_FIELD_DEFAULT_OPTIONS in the root
injector. Importing the token kept @angular/material/form-field in
main.js, and Material 22.2's form field imports @angular/forms/signals,
which would add @angular/forms to startup as well (the reason the
Angular 22.2 dependency update exceeds the initial-bytes ratchet). Every
form field renders in a lazy chunk, so the outline + dynamic defaults
now come from provideFormFieldDefaults()
(@iptvnator/ui/components/form-field-defaults) on each component that
renders <mat-form-field>.

tools/nx/check-form-field-defaults.mjs (ui:form-field-defaults:validate,
run in CI) reports a component that renders a form field without the
provider, and any use of the token outside the shared provider. The
Xtream import form test asserts outlined, dynamically sized fields with
no root provider; without the component provider they render as fill.

renderer.initialBytes: 1,596,201 -> 1,534,604 bytes (-61,597) on a local
production build; the baseline is lowered to the measured value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 08:39:00 +02:00

166 lines
6.0 KiB
TypeScript

import {
HttpClient,
provideHttpClient,
withInterceptorsFromDi,
withXhr,
} from '@angular/common/http';
import {
FullscreenOverlayContainer,
OverlayContainer,
} from '@angular/cdk/overlay';
import { ApplicationConfig, inject, importProvidersFrom } from '@angular/core';
import { provideAnimations } from '@angular/platform-browser/animations';
import { provideRouter, withComponentInputBinding } from '@angular/router';
import { provideServiceWorker } from '@angular/service-worker';
import { provideEffects } from '@ngrx/effects';
import { provideRouterStore, routerReducer } from '@ngrx/router-store';
import { provideStore } from '@ngrx/store';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { PlaylistEffects, playlistReducer } from '@iptvnator/m3u-state';
import { NgxIndexedDBModule } from 'ngx-indexed-db';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import {
PORTAL_EXTERNAL_PLAYBACK,
PORTAL_PLAYER,
} from '@iptvnator/portal/shared/util/tokens';
import { STALKER_PLAYLIST_CONNECTION_EDITOR } from '@iptvnator/playlist/shared/ui/stalker-connection-editor';
import { provideXtreamDataSource } from '@iptvnator/portal/xtream/data-access';
import {
provideParentalLockPlaylistCleanup,
DataService,
} from '@iptvnator/services';
import { dbConfig } from '@iptvnator/shared/interfaces';
import { changeDetectionProviders } from '../environments/change-detection.providers';
import { storeDevtoolsProviders } from '../environments/store-devtools.providers';
import { routes } from './app.routes';
import { ElectronService } from './services/electron.service';
import { ExternalPlaybackService } from './services/external-playback.service';
import { PlayerService } from './services/player.service';
import { provideParentalLockPrompt } from './services/parental-lock-prompt.service';
import { providePortalPlaybackPositions } from './services/portal-playback-positions.service';
import { PwaService } from './services/pwa.service';
import { shouldEnableServiceWorker } from './services/runtime-config';
import { LazyStalkerPlaylistConnectionEditor } from './services/lazy-stalker-playlist-connection-editor';
import { provideWorkspaceShellActions } from './services/workspace-shell-actions.service';
// AoT requires an exported function for factories
export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader {
return new TranslateHttpLoader(http, './assets/i18n/', '.json');
}
/**
* Synchronously read the user's preferred language from localStorage so the
* very first render uses the right locale instead of flashing English first
* and re-rendering once the IDB-backed settings finish loading.
*
* The hint is written by AppComponent.initSettings() whenever the saved
* settings resolve, so first-ever boots fall back to English (no hint
* present yet) and every subsequent boot uses the saved language.
*/
const SUPPORTED_LANGS = new Set([
'ar',
'ary',
'by',
'de',
'el',
'en',
'es',
'fr',
'hu',
'it',
'ja',
'ko',
'nl',
'pl',
'pt',
'ru',
'tr',
'zh',
'zhtw',
]);
export const PREFERRED_LANGUAGE_STORAGE_KEY = 'iptvnator:preferred-language';
export function getInitialLanguage(): string {
try {
const saved = localStorage.getItem(PREFERRED_LANGUAGE_STORAGE_KEY);
if (saved && SUPPORTED_LANGS.has(saved)) {
return saved;
}
} catch {
// localStorage may throw in privacy modes; fall through to default.
}
return 'en';
}
/**
* Conditionally provides the necessary service based on the current environment
*/
export function DataFactory() {
if (window.electron) {
return inject(ElectronService);
}
return inject(PwaService);
}
export const appConfig: ApplicationConfig = {
providers: [
...changeDetectionProviders,
provideRouter(routes, withComponentInputBinding()),
provideAnimations(),
// CDK overlays (menus, tooltips, dialogs) live in a container under
// <body>, which the browser hides behind an element in DOM fullscreen.
// This container follows the fullscreen element instead, so the
// fullscreen channel panel's sort/context menus and the controls'
// tooltips render while the player is fullscreen.
{ provide: OverlayContainer, useClass: FullscreenOverlayContainer },
provideHttpClient(withXhr(), withInterceptorsFromDi()),
provideStore({
router: routerReducer,
playlistState: playlistReducer,
}),
provideEffects([PlaylistEffects]),
provideRouterStore(),
...storeDevtoolsProviders,
provideServiceWorker('ngsw-worker.js', {
enabled: shouldEnableServiceWorker(),
registrationStrategy: 'registerWhenStable:30000',
}),
importProvidersFrom(
NgxIndexedDBModule.forRoot(dbConfig),
NgxSkeletonLoaderModule.forRoot({
animation: 'pulse',
loadingText: 'This item is actually loading...',
}),
TranslateModule.forRoot({
defaultLanguage: getInitialLanguage(),
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient],
},
})
),
{
provide: DataService,
useFactory: DataFactory,
},
{
provide: PORTAL_PLAYER,
useExisting: PlayerService,
},
{
provide: PORTAL_EXTERNAL_PLAYBACK,
useExisting: ExternalPlaybackService,
},
...providePortalPlaybackPositions(),
{
provide: STALKER_PLAYLIST_CONNECTION_EDITOR,
useExisting: LazyStalkerPlaylistConnectionEditor,
},
...provideWorkspaceShellActions(),
...provideParentalLockPrompt(),
provideParentalLockPlaylistCleanup(),
...provideXtreamDataSource(),
],
};