Files
iptvnator/apps/web/src/app/settings/settings-playback-section.component.ts
T
ec4b9286a7 perf(web): provide form-field defaults per component, not at the root (#1928)
* 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>

* perf(web): cite #1928 as the initial-bytes baseline evidence

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(ui): check form-field providers per component on the TypeScript AST

The guard matched templates and providers with regular expressions over
the whole file: a quoted inline template went unread, one component's
provider covered another in the same file, an earlier array-valued
provider hid the call, and provideFormFieldDefaults() in app.config or a
route passed although it loads Material's form field on startup again.
It now reads each @Component decorator's own template and providers
array, and reports any provideFormFieldDefaults() call outside one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: 4gray <fourgray@proton.me>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:53:08 +02:00

126 lines
4.5 KiB
TypeScript

import { CommonModule } from '@angular/common';
import {
Component,
input,
output,
ViewEncapsulation,
ChangeDetectionStrategy,
} from '@angular/core';
import { FormGroup, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { MatTooltipModule } from '@angular/material/tooltip';
import { TranslateModule } from '@ngx-translate/core';
import {
StreamFormat,
VideoPlayer,
reportsPlaybackFailures,
} from '@iptvnator/shared/interfaces';
import { SettingsPlayerOption } from './settings.models';
import { markSectionForCheckOnFormEvents } from './settings-section-form-render';
import { provideFormFieldDefaults } from '@iptvnator/ui/components/form-field-defaults';
@Component({
selector: 'app-settings-playback-section',
providers: [provideFormFieldDefaults()],
imports: [
CommonModule,
MatButtonModule,
MatFormFieldModule,
MatIconModule,
MatInputModule,
MatSelectModule,
MatSlideToggleModule,
MatTooltipModule,
ReactiveFormsModule,
TranslateModule,
],
templateUrl: './settings-playback-section.component.html',
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
styles: [':host { display: contents; }'],
})
export class SettingsPlaybackSectionComponent {
readonly mpvPlayerArgumentsPlaceholder = [
'--ontop',
'--autofit=640x360',
'--geometry=+80+80',
].join('\n');
readonly vlcPlayerArgumentsPlaceholder = [
'--video-on-top',
'--width=640',
'--height=360',
].join('\n');
readonly form = input.required<FormGroup>();
constructor() {
// Parent patches (Discard, backup import) change the form outside
// this OnPush section's events.
markSectionForCheckOnFormEvents(this.form);
}
readonly players = input.required<SettingsPlayerOption[]>();
readonly streamFormatEnum = input.required<typeof StreamFormat>();
readonly isDesktop = input(false);
/** Frame-copy embedded MPV engine is possible on this machine */
readonly frameCopyAvailable = input(false);
/** Frame-copy engine is what the current app run actually uses */
readonly frameCopyActive = input(false);
readonly supportsManagedExternalPlayers = input(false);
readonly supportsExternalPlayerPathSettings = input(false);
/**
* Cross-playlist movie matching is Electron-only, so the auto-failover
* toggle would control nothing in the PWA.
*/
readonly supportsVodMultiSource = input(false);
readonly selectRecordingFolder = output<void>();
isWebPlayerSelected(): boolean {
return reportsPlaybackFailures(this.form().value.player);
}
/**
* The fullscreen channel panel lives inside the fullscreen element the
* shared controls own (the player view host). The legacy vendor chrome
* fullscreens the engine's own element and external MPV/VLC own their
* own window, so in both cases the toggle would control nothing.
* Embedded MPV always renders the shared controls.
*/
supportsFullscreenChannelPanel(): boolean {
const value = this.form().value;
return (
(this.isWebPlayerSelected() &&
value.webPlayerSharedControls !== false) ||
value.player === VideoPlayer.EmbeddedMpv
);
}
/**
* The Up next card is part of the shared controls. Embedded MPV mounts
* them only under the frame-copy engine; the native-view engine keeps
* its own dock, where the toggle would control nothing.
*/
supportsUpNextCard(): boolean {
const value = this.form().value;
return (
(this.isWebPlayerSelected() &&
value.webPlayerSharedControls !== false) ||
(value.player === VideoPlayer.EmbeddedMpv && this.frameCopyActive())
);
}
isExternalPlayerSelected(): boolean {
const player = this.form().value.player;
return player === VideoPlayer.MPV || player === VideoPlayer.VLC;
}
isEmbeddedMpvSelected(): boolean {
return this.form().value.player === VideoPlayer.EmbeddedMpv;
}
}