diff --git a/.changes/backup-pwa-xtream-user-state.md b/.changes/backup-pwa-xtream-user-state.md new file mode 100644 index 000000000..a2e8acec6 --- /dev/null +++ b/.changes/backup-pwa-xtream-user-state.md @@ -0,0 +1,6 @@ +--- +type: fix +area: backup +--- + +Backups made in the browser and self-hosted versions now include Xtream favorites, recently viewed items and resume positions. Restoring such a backup brings them back right away, under whichever playlist the portal is imported as. diff --git a/.changes/performance-lazy-form-field-defaults.md b/.changes/performance-lazy-form-field-defaults.md new file mode 100644 index 000000000..d1ae58790 --- /dev/null +++ b/.changes/performance-lazy-form-field-defaults.md @@ -0,0 +1,7 @@ +--- +type: perf +area: performance +--- + +The app loads less code at startup: the text field component and its form +helpers now load with the screens that show a form instead of with the app. diff --git a/.changes/portals-hide-na-metadata.md b/.changes/portals-hide-na-metadata.md new file mode 100644 index 000000000..baace44be --- /dev/null +++ b/.changes/portals-hide-na-metadata.md @@ -0,0 +1,6 @@ +--- +type: fix +area: portals +--- + +Movie and series details and catalog cards from Stalker and Xtream portals no longer show "N/A", "-" or a zero rating in place of missing information. Fields the provider left empty are simply hidden, and TMDB can now fill in the rating and release year for them. diff --git a/.changes/search-single-debounce.md b/.changes/search-single-debounce.md new file mode 100644 index 000000000..1b00e5520 --- /dev/null +++ b/.changes/search-single-debounce.md @@ -0,0 +1,6 @@ +--- +type: perf +area: search +--- + +Search results appear about twice as fast after you stop typing: the search now waits once for typing to pause instead of twice. diff --git a/.changes/stalker-series-discover-chips.md b/.changes/stalker-series-discover-chips.md new file mode 100644 index 000000000..db6945f23 --- /dev/null +++ b/.changes/stalker-series-discover-chips.md @@ -0,0 +1,6 @@ +--- +type: fix +area: stalker +--- + +Stalker series details now keep their clickable genre and country chips after TMDB fills in the details, so you can jump to Discover from them as you can for movies. diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 965668988..6b4853bf0 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,10 +1,11 @@ # Every Dependabot PR triggers the full pipeline (~15 jobs), so version # updates are batched weekly. Nx minor+patch updates use a dedicated group so # official packages move together; Nx majors are manual coordinated migrations. -# Native packaging, parser, and version-locked playback dependencies stay in -# standalone PRs; other minor+patch updates are grouped per ecosystem. Security -# updates are separate and are not limited by this schedule; CI enforces complete -# Nx lockstep. +# Electron majors are held while Linux armv7l packages ship: Electron 44 +# dropped the linux-armv7l binary. Native packaging, parser, and +# version-locked playback dependencies stay in standalone PRs; other +# minor+patch updates are grouped per ecosystem. Security updates are separate +# and are not limited by this schedule; CI enforces complete Nx lockstep. version: 2 updates: - package-ecosystem: npm @@ -21,6 +22,9 @@ updates: - dependency-name: '@nx/*' update-types: - version-update:semver-major + - dependency-name: electron + update-types: + - version-update:semver-major groups: nx-version-updates: applies-to: version-updates diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f5a2e1418..d25580c26 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -353,6 +353,8 @@ jobs: --only open-source/renderer.layoutShiftScore --only playback/renderer.httpRequestsToPlaying --only playback/renderer.layoutShiftScore + --only search/renderer.ipcCallsPerSearch + --only search/renderer.sqlStatementsPerSearch - name: Upload journey summaries if: always() @@ -428,6 +430,12 @@ jobs: - name: Validate icon-only button names run: pnpm run a11y:icon-buttons:validate + # Form-field defaults are provided per lazy component: the root + # token import would load Material form-field and @angular/forms + # on startup (tools/nx/check-form-field-defaults.mjs). + - name: Validate form-field defaults + run: pnpm run ui:form-field-defaults:validate + # Nx rejects a non-parallel task with continuous dependencies, and # the Playwright plugin infers serve dependencies only when CI is # unset, so this checks both the local and the CI inference. diff --git a/apps/electron-backend-e2e/src/journeys/search-journey-app.ts b/apps/electron-backend-e2e/src/journeys/search-journey-app.ts index 7d76b54fe..b57418091 100644 --- a/apps/electron-backend-e2e/src/journeys/search-journey-app.ts +++ b/apps/electron-backend-e2e/src/journeys/search-journey-app.ts @@ -60,7 +60,7 @@ export const SEARCH_JOURNEY_PORTAL_NAME = 'Journey search portal'; * matches 170 series titles, more than the first page of 100. */ export const SEARCH_JOURNEY_QUERY = 'system'; -/** Well below the shell's 350 ms input debounce, like steady typing. */ +/** Well below the shell's 300 ms input debounce, like steady typing. */ export const SEARCH_JOURNEY_KEY_DELAY_MS = 100; const QUIET_MS = 1_000; const QUIET_POLL_MS = 100; diff --git a/apps/electron-backend-e2e/src/performance/search-journey-record.ts b/apps/electron-backend-e2e/src/performance/search-journey-record.ts index 556f378e8..0e941379c 100644 --- a/apps/electron-backend-e2e/src/performance/search-journey-record.ts +++ b/apps/electron-backend-e2e/src/performance/search-journey-record.ts @@ -34,7 +34,7 @@ export const SEARCH_JOURNEY_UNAVAILABLE_COUNTERS: Readonly< export const SEARCH_JOURNEY_QUERY_METHOD = 'dbGlobalSearch'; /** - * Longest accepted gap between two keydowns. Below the shell's 350 ms input + * Longest accepted gap between two keydowns. Below the shell's 300 ms input * debounce, so a late key on a busy machine cannot let an intermediate term * run a query that steady typing would not. */ diff --git a/apps/web/src/app/app.config.ts b/apps/web/src/app/app.config.ts index 77a486595..c7a748202 100644 --- a/apps/web/src/app/app.config.ts +++ b/apps/web/src/app/app.config.ts @@ -9,7 +9,6 @@ import { OverlayContainer, } from '@angular/cdk/overlay'; import { ApplicationConfig, inject, importProvidersFrom } from '@angular/core'; -import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field'; import { provideRouter, withComponentInputBinding } from '@angular/router'; import { provideServiceWorker } from '@angular/service-worker'; import { provideEffects } from '@ngrx/effects'; @@ -165,9 +164,5 @@ export const appConfig: ApplicationConfig = { ...provideParentalLockPrompt(), provideParentalLockPlaylistCleanup(), ...provideXtreamDataSource(), - { - provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, - useValue: { appearance: 'outline', subscriptSizing: 'dynamic' }, - }, ], }; diff --git a/apps/web/src/app/settings/settings-about-section.component.ts b/apps/web/src/app/settings/settings-about-section.component.ts index 511206c62..d48af2add 100644 --- a/apps/web/src/app/settings/settings-about-section.component.ts +++ b/apps/web/src/app/settings/settings-about-section.component.ts @@ -20,6 +20,7 @@ import { } from '@iptvnator/shared/interfaces'; import { UpdateChannelOption } from './settings.models'; import { markSectionForCheckOnFormEvents } from './settings-section-form-render'; +import { provideFormFieldDefaults } from '@iptvnator/ui/components/form-field-defaults'; interface SettingsUpdateStatusChip { readonly labelKey: string; @@ -28,6 +29,7 @@ interface SettingsUpdateStatusChip { @Component({ selector: 'app-settings-about-section', + providers: [provideFormFieldDefaults()], imports: [ MatButtonModule, MatFormFieldModule, diff --git a/apps/web/src/app/settings/settings-epg-section.component.ts b/apps/web/src/app/settings/settings-epg-section.component.ts index d4ec29556..431a94796 100644 --- a/apps/web/src/app/settings/settings-epg-section.component.ts +++ b/apps/web/src/app/settings/settings-epg-section.component.ts @@ -23,9 +23,11 @@ import { EpgSourceStatusComponent } from '@iptvnator/ui/epg'; import { TranslateModule } from '@ngx-translate/core'; import { EpgViewModeOption } from './settings.models'; import { markSectionForCheckOnFormEvents } from './settings-section-form-render'; +import { provideFormFieldDefaults } from '@iptvnator/ui/components/form-field-defaults'; @Component({ selector: 'app-settings-epg-section', + providers: [provideFormFieldDefaults()], imports: [ EpgSourceStatusComponent, MatButtonModule, diff --git a/apps/web/src/app/settings/settings-general-section.component.ts b/apps/web/src/app/settings/settings-general-section.component.ts index 7bb7b4f33..92582b33c 100644 --- a/apps/web/src/app/settings/settings-general-section.component.ts +++ b/apps/web/src/app/settings/settings-general-section.component.ts @@ -20,9 +20,11 @@ import { ThemeOption, } from './settings.models'; import { markSectionForCheckOnFormEvents } from './settings-section-form-render'; +import { provideFormFieldDefaults } from '@iptvnator/ui/components/form-field-defaults'; @Component({ selector: 'app-settings-general-section', + providers: [provideFormFieldDefaults()], imports: [ CommonModule, MatFormFieldModule, diff --git a/apps/web/src/app/settings/settings-parental-section.component.ts b/apps/web/src/app/settings/settings-parental-section.component.ts index 7e874c010..50ced0a82 100644 --- a/apps/web/src/app/settings/settings-parental-section.component.ts +++ b/apps/web/src/app/settings/settings-parental-section.component.ts @@ -9,9 +9,11 @@ import { } from '@angular/material/slide-toggle'; import { TranslateModule } from '@ngx-translate/core'; import { ParentalLockRelockMinutes } from '@iptvnator/shared/interfaces'; +import { provideFormFieldDefaults } from '@iptvnator/ui/components/form-field-defaults'; @Component({ selector: 'app-settings-parental-lock-section', + providers: [provideFormFieldDefaults()], imports: [ MatButtonModule, MatFormFieldModule, diff --git a/apps/web/src/app/settings/settings-playback-section.component.ts b/apps/web/src/app/settings/settings-playback-section.component.ts index 5fd306029..b806917b2 100644 --- a/apps/web/src/app/settings/settings-playback-section.component.ts +++ b/apps/web/src/app/settings/settings-playback-section.component.ts @@ -22,9 +22,11 @@ import { } 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, diff --git a/apps/web/src/app/settings/settings-remote-control-section.component.ts b/apps/web/src/app/settings/settings-remote-control-section.component.ts index 3b69c0636..9e99ac088 100644 --- a/apps/web/src/app/settings/settings-remote-control-section.component.ts +++ b/apps/web/src/app/settings/settings-remote-control-section.component.ts @@ -15,9 +15,11 @@ import { MatTooltipModule } from '@angular/material/tooltip'; import { TranslateModule } from '@ngx-translate/core'; import { QRCodeComponent } from 'angularx-qrcode'; import { markSectionForCheckOnFormEvents } from './settings-section-form-render'; +import { provideFormFieldDefaults } from '@iptvnator/ui/components/form-field-defaults'; @Component({ selector: 'app-settings-remote-control-section', + providers: [provideFormFieldDefaults()], imports: [ MatButtonModule, MatFormFieldModule, diff --git a/apps/web/src/app/settings/settings-tmdb-section.component.ts b/apps/web/src/app/settings/settings-tmdb-section.component.ts index 428893053..5a751e376 100644 --- a/apps/web/src/app/settings/settings-tmdb-section.component.ts +++ b/apps/web/src/app/settings/settings-tmdb-section.component.ts @@ -17,11 +17,13 @@ import { TranslateModule } from '@ngx-translate/core'; import { TmdbApiService, TmdbCacheService } from '@iptvnator/services'; import type { TmdbCacheStats } from '@iptvnator/shared/interfaces'; import { markSectionForCheckOnFormEvents } from './settings-section-form-render'; +import { provideFormFieldDefaults } from '@iptvnator/ui/components/form-field-defaults'; type TmdbKeyTestState = 'idle' | 'testing' | 'success' | 'error'; @Component({ selector: 'app-settings-tmdb-section', + providers: [provideFormFieldDefaults()], imports: [ MatButtonModule, MatFormFieldModule, diff --git a/apps/web/src/m3-theme.scss b/apps/web/src/m3-theme.scss index acd62acd0..ccf773f13 100644 --- a/apps/web/src/m3-theme.scss +++ b/apps/web/src/m3-theme.scss @@ -279,8 +279,9 @@ html { // ─── Global refined outlined inputs ────────────────────────────────────────── // Applied app-wide AFTER mat.all-component-themes so these overrides win the -// cascade. Pairs with MAT_FORM_FIELD_DEFAULT_OPTIONS (outline + dynamic) set -// in app.config.ts. +// cascade. Pairs with the outline + dynamic MAT_FORM_FIELD_DEFAULT_OPTIONS +// that provideFormFieldDefaults() (@iptvnator/ui/components) sets on each +// component rendering a form field. $app-refined-input-density: mat.define-theme(( density: (scale: -2), )); diff --git a/docs/architecture/embedded-inline-playback.md b/docs/architecture/embedded-inline-playback.md index 628a4ad89..64cbe46f9 100644 --- a/docs/architecture/embedded-inline-playback.md +++ b/docs/architecture/embedded-inline-playback.md @@ -157,6 +157,14 @@ Contracts: - Hosts provide hero chips/meta/actions as `*appDetailTags` / `*appDetailMeta` / `*appDetailActions` templates; the shell stamps them into the hero and again into the About block. Degradation stays "missing → not rendered". +- Provider stand-ins count as missing: `N/A` (any case), a lone `-`, and a + `0` IMDb/Kinopoisk score are dropped where provider rows are mapped, by + `readProviderText` / `readProviderRating` + (`libs/shared/interfaces/src/lib/provider-metadata.util.ts`) — in + `createStalkerInfo()`, the `vod-details-adapters.ts` normalizers and the + Xtream store's detail selection. Templates keep plain `@if` guards; do not + teach them the placeholder list. Widen the list only with evidence of a + provider sending the new stand-in. - The shell never wraps the `[detail-player]` slot in a conditional; the host's `@if (inlinePlayback())` is the only creator/destroyer of the player subtree, so shell state changes cannot recreate the `