diff --git a/docs/superpowers/plans/2026-07-17-web-player-shared-controls-setting.md b/docs/superpowers/plans/2026-07-17-web-player-shared-controls-setting.md new file mode 100644 index 000000000..771b355c7 --- /dev/null +++ b/docs/superpowers/plans/2026-07-17-web-player-shared-controls-setting.md @@ -0,0 +1,898 @@ +# Web-Player Shared Controls Setting Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add a persisted experimental Playback checkbox that enables the existing shared controls for newly created HTML5, Video.js, and ArtPlayer sessions without restarting IPTVnator. + +**Architecture:** `SettingsStore` owns a default-off `webPlayerSharedControls` boolean and the Settings form exposes it only for the three web engines. `WebPlayerViewComponent` resolves the saved value into a component-scoped, immutable `WEB_PLAYER_SHARED_CONTROLS` snapshot, preserving each engine's construction-time legacy/shared split while leaving Embedded MPV and external players unchanged. + +**Tech Stack:** Angular standalone components and reactive forms, NgRx Signal Store, Angular DI, Angular Material, ngx-translate JSON locales, Jest/TestBed, Playwright, Nx. + +--- + +## File Map + +### Settings contract and persistence + +- Modify `libs/shared/interfaces/src/lib/settings.interface.ts` + - Declare the optional persisted boolean. +- Modify `libs/services/src/lib/settings-store.service.ts` + - Add the default, normalize stored input, and include the resolved value in + complete settings serialization. +- Modify `libs/services/src/lib/settings-store.service.spec.ts` + - Cover missing, enabled, malformed, and persisted values. + +### Settings form and presentation + +- Modify `apps/web/src/app/settings/settings-form.utils.ts` + - Add the form control and form-to-settings mapping. +- Modify `apps/web/src/app/settings/settings-playback-section.component.ts` + - Identify whether the selected engine is one of the three web players. +- Modify `apps/web/src/app/settings/settings-playback-section.component.html` + - Render the conditional Material checkbox. +- Modify `apps/web/src/app/settings/settings-playback-section.component.spec.ts` + - Cover visibility and checkbox binding. +- Modify `apps/web/src/app/settings/settings.component.spec.ts` + - Keep the canonical test fixture shape current and verify hydration/save. +- Modify all JSON files under `apps/web/src/assets/i18n/` + - Add the same two `SETTINGS` keys with localized values. + +### Playback session wiring + +- Modify `libs/ui/playback/src/lib/web-player-view/web-player-view.component.ts` + - Provide an immutable, settings-backed controls token per player host. +- Modify + `libs/ui/playback/src/lib/web-player-view/web-player-view.component.shared-controls.spec.ts` + - Verify enabled/disabled resolution and per-host snapshot behavior. + +### End-to-end coverage and documentation + +- Modify `apps/web-e2e/src/settings.e2e.ts` + - Verify browser persistence after Save and reload. +- Modify `apps/electron-backend-e2e/src/settings.e2e.ts` + - Verify desktop persistence across restart and shared controls on the next + HTML5 playback session. +- Modify `README.md` + - Mention the optional unified built-in web-player controls. +- Modify `docs/architecture/player-controls-contract.md` + - Replace compile-time-only rollout language with the persisted setting and + per-session snapshot. +- Modify `AGENTS.md` and `CLAUDE.md` + - Keep their shared-controls descriptions synchronized. + +## Task 1: Persist the Default-Off Settings Contract + +**Files:** + +- Modify: `libs/shared/interfaces/src/lib/settings.interface.ts` +- Modify: `libs/services/src/lib/settings-store.service.ts` +- Test: `libs/services/src/lib/settings-store.service.spec.ts` + +- [ ] **Step 1: Write failing store behavior tests** + +Add these cases to `libs/services/src/lib/settings-store.service.spec.ts`. +The temporary intersections keep the red test compiling before the shared +interface declares the field: + +```ts +it('defaults shared web controls to false when the stored field is missing', async () => { + const store = injector.get(SettingsStore); + + await store.loadSettings(); + + const settings = store.getSettings() as ReturnType< + typeof store.getSettings + > & { + webPlayerSharedControls?: unknown; + }; + expect(settings.webPlayerSharedControls).toBe(false); +}); + +it('restores an enabled shared web controls preference', async () => { + storedSettings = { + webPlayerSharedControls: true, + } as Partial & { webPlayerSharedControls: boolean }; + const store = injector.get(SettingsStore); + + await store.loadSettings(); + + const settings = store.getSettings() as ReturnType< + typeof store.getSettings + > & { + webPlayerSharedControls?: unknown; + }; + expect(settings.webPlayerSharedControls).toBe(true); +}); + +it('normalizes a malformed shared web controls preference to false', async () => { + storedSettings = { + webPlayerSharedControls: 'enabled', + } as unknown as Partial; + const store = injector.get(SettingsStore); + + await store.loadSettings(); + + const settings = store.getSettings() as ReturnType< + typeof store.getSettings + > & { + webPlayerSharedControls?: unknown; + }; + expect(settings.webPlayerSharedControls).toBe(false); +}); + +it('persists the resolved shared web controls preference', async () => { + const store = injector.get(SettingsStore); + + await store.updateSettings({ + webPlayerSharedControls: true, + } as Partial & { webPlayerSharedControls: boolean }); + + expect(storage.set).toHaveBeenCalledWith( + STORE_KEY.Settings, + expect.objectContaining({ + webPlayerSharedControls: true, + }) + ); +}); +``` + +- [ ] **Step 2: Run the focused store test and verify the red state** + +Run: + +```bash +pnpm nx test services --skip-nx-cache --runInBand \ + --testPathPatterns=settings-store.service.spec +``` + +Expected: FAIL because `getSettings()` omits +`webPlayerSharedControls`, and the preference is not persisted. + +- [ ] **Step 3: Add the shared settings field** + +Add this property beside `player` in +`libs/shared/interfaces/src/lib/settings.interface.ts`: + +```ts +/** + * Use IPTVnator's shared controls in HTML5, Video.js, and ArtPlayer. + * Missing values remain off for compatibility with older saved settings. + */ +webPlayerSharedControls?: boolean; +``` + +- [ ] **Step 4: Normalize and serialize the setting** + +In `libs/services/src/lib/settings-store.service.ts`, add the default: + +```ts +webPlayerSharedControls: false, +``` + +Insert it immediately after `player: VideoPlayer.VideoJs` in +`DEFAULT_SETTINGS`. After `...storedSettings` in `loadSettings()`, override +untrusted stored input with an exact boolean: + +```ts +patchState(store, { + ...DEFAULT_SETTINGS, + ...storedSettings, + webPlayerSharedControls: storedSettings.webPlayerSharedControls === true, + dashboardRails: normalizeDashboardRailsSettings( + storedSettings.dashboardRails + ), +}); +``` + +Include the resolved field in `getSettings()`: + +```ts +webPlayerSharedControls: + store.webPlayerSharedControls?.() === true, +``` + +Insert it immediately after `player: store.player()`. + +Once the interface exists, simplify the four new tests to use direct typed +access and typed updates: + +```ts +expect(store.getSettings().webPlayerSharedControls).toBe(false); +expect(store.getSettings().webPlayerSharedControls).toBe(true); +await store.updateSettings({ webPlayerSharedControls: true }); +``` + +Keep the `unknown as Partial` cast only in the malformed-value test. + +- [ ] **Step 5: Run contract and store tests** + +Run: + +```bash +pnpm nx build shared-interfaces +pnpm nx test services --skip-nx-cache --runInBand \ + --testPathPatterns=settings-store.service.spec +``` + +Expected: both commands PASS; missing/malformed values resolve to `false`, and +`true` is restored and persisted. + +- [ ] **Step 6: Commit the settings contract** + +```bash +git add libs/shared/interfaces/src/lib/settings.interface.ts \ + libs/services/src/lib/settings-store.service.ts \ + libs/services/src/lib/settings-store.service.spec.ts +git commit -m "feat(settings): persist shared web controls preference" +``` + +## Task 2: Add the Conditional Playback Checkbox + +**Files:** + +- Modify: `apps/web/src/app/settings/settings-form.utils.ts` +- Modify: `apps/web/src/app/settings/settings-playback-section.component.ts` +- Modify: `apps/web/src/app/settings/settings-playback-section.component.html` +- Test: `apps/web/src/app/settings/settings-playback-section.component.spec.ts` +- Test: `apps/web/src/app/settings/settings.component.spec.ts` +- Modify: `apps/web/src/assets/i18n/ar.json` +- Modify: `apps/web/src/assets/i18n/ary.json` +- Modify: `apps/web/src/assets/i18n/by.json` +- Modify: `apps/web/src/assets/i18n/de.json` +- Modify: `apps/web/src/assets/i18n/el.json` +- Modify: `apps/web/src/assets/i18n/en.json` +- Modify: `apps/web/src/assets/i18n/es.json` +- Modify: `apps/web/src/assets/i18n/fr.json` +- Modify: `apps/web/src/assets/i18n/it.json` +- Modify: `apps/web/src/assets/i18n/ja.json` +- Modify: `apps/web/src/assets/i18n/ko.json` +- Modify: `apps/web/src/assets/i18n/nl.json` +- Modify: `apps/web/src/assets/i18n/pl.json` +- Modify: `apps/web/src/assets/i18n/pt.json` +- Modify: `apps/web/src/assets/i18n/ru.json` +- Modify: `apps/web/src/assets/i18n/tr.json` +- Modify: `apps/web/src/assets/i18n/zh.json` +- Modify: `apps/web/src/assets/i18n/zhtw.json` + +- [ ] **Step 1: Write failing visibility and binding tests** + +Add `webPlayerSharedControls` to the local `createForm()` helper in +`settings-playback-section.component.spec.ts`: + +```ts +webPlayerSharedControls: new FormControl(false), +``` + +Add the following tests: + +```ts +it.each([VideoPlayer.VideoJs, VideoPlayer.Html5Player, VideoPlayer.ArtPlayer])( + 'shows shared controls for the %s web player', + (player) => { + fixture.componentRef.setInput('form', createForm(player)); + fixture.detectChanges(); + + expect( + fixture.nativeElement.querySelector( + '[data-test-id="web-player-shared-controls-setting"]' + ) + ).not.toBeNull(); + } +); + +it.each([VideoPlayer.EmbeddedMpv, VideoPlayer.MPV, VideoPlayer.VLC])( + 'hides shared web controls for the %s player', + (player) => { + fixture.componentRef.setInput('form', createForm(player)); + fixture.detectChanges(); + + expect( + fixture.nativeElement.querySelector( + '[data-test-id="web-player-shared-controls-setting"]' + ) + ).toBeNull(); + } +); + +it('binds the shared controls checkbox to the settings form', () => { + const form = createForm(VideoPlayer.VideoJs); + form.controls['webPlayerSharedControls'].setValue(true); + fixture.componentRef.setInput('form', form); + fixture.detectChanges(); + + expect( + fixture.nativeElement.querySelector( + '[data-test-id="web-player-shared-controls-toggle"] input[type="checkbox"]' + )?.checked + ).toBe(true); +}); +``` + +In `settings.component.spec.ts`, add this field to `DEFAULT_SETTINGS`: + +```ts +webPlayerSharedControls: false, +``` + +Add explicit hydration and save assertions: + +```ts +it('hydrates the shared web controls preference', () => { + const mockStore = settingsStore as unknown as MockSettingsStore; + mockStore._setSettings({ + webPlayerSharedControls: true, + }); + + component.setSettings(); + + expect(component.settingsForm.get('webPlayerSharedControls')?.value).toBe( + true + ); +}); + +it('saves the shared web controls preference', async () => { + const mockStore = settingsStore as unknown as MockSettingsStore; + mockStore.updateSettings.mockResolvedValue(undefined); + + component.settingsForm.get('webPlayerSharedControls')?.setValue(true); + component.onSubmit(); + await fixture.whenStable(); + + expect(mockStore.updateSettings).toHaveBeenCalledWith( + expect.objectContaining({ + webPlayerSharedControls: true, + }) + ); +}); +``` + +- [ ] **Step 2: Run focused web tests and verify the red state** + +Run: + +```bash +pnpm nx test web --skip-nx-cache --runInBand \ + --testPathPatterns='settings-playback-section.component.spec|settings.component.spec' +``` + +Expected: FAIL because the production form and Playback section do not yet +declare or render the preference. + +- [ ] **Step 3: Add form hydration and serialization** + +In `createSettingsForm()` in +`apps/web/src/app/settings/settings-form.utils.ts`, add: + +```ts +webPlayerSharedControls: false, +``` + +Insert it immediately after the existing `player` control. + +In `createSettingsFromFormValue()`, add: + +```ts +webPlayerSharedControls: value.webPlayerSharedControls ?? false, +``` + +Insert it immediately after the existing `player` mapping. + +The existing `SettingsComponent.setSettings()` `patchValue()` call then +hydrates the checkbox without a new component-specific code path. + +- [ ] **Step 4: Add the selected-engine predicate and checkbox** + +Add this method to +`apps/web/src/app/settings/settings-playback-section.component.ts`: + +```ts +isWebPlayerSelected(): boolean { + const player = this.form().value.player; + return ( + player === VideoPlayer.VideoJs || + player === VideoPlayer.Html5Player || + player === VideoPlayer.ArtPlayer + ); +} +``` + +Insert this row in +`apps/web/src/app/settings/settings-playback-section.component.html` after the +stream-format selector: + +```html +@if (isWebPlayerSelected()) { +
+
+

{{ 'SETTINGS.WEB_PLAYER_SHARED_CONTROLS' | translate }}

+

+ {{ 'SETTINGS.WEB_PLAYER_SHARED_CONTROLS_DESCRIPTION' | translate }} +

+
+
+ +
+
+} +``` + +- [ ] **Step 5: Run focused web tests and verify the green state** + +Run: + +```bash +pnpm nx test web --skip-nx-cache --runInBand \ + --testPathPatterns='settings-playback-section.component.spec|settings.component.spec' +``` + +Expected: PASS for all three visible web players, all three hidden non-web +players, form hydration, and Save serialization. + +- [ ] **Step 6: Add exact locale values** + +Add `WEB_PLAYER_SHARED_CONTROLS` and +`WEB_PLAYER_SHARED_CONTROLS_DESCRIPTION` beside +`VIDEO_PLAYER_DESCRIPTION` inside `SETTINGS` in all 18 locale files: + +| Locale | `WEB_PLAYER_SHARED_CONTROLS` | `WEB_PLAYER_SHARED_CONTROLS_DESCRIPTION` | +| ------ | ---------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| `en` | Unified controls for web players (experimental) | Use IPTVnator's shared controls in HTML5, Video.js, and ArtPlayer. | +| `ru` | Единые элементы управления для веб-плееров (экспериментально) | Использовать общие элементы управления IPTVnator в HTML5, Video.js и ArtPlayer. | +| `de` | Einheitliche Steuerung für Web-Player (experimentell) | Verwendet die gemeinsamen IPTVnator-Steuerelemente in HTML5, Video.js und ArtPlayer. | +| `es` | Controles unificados para reproductores web (experimental) | Usa los controles compartidos de IPTVnator en HTML5, Video.js y ArtPlayer. | +| `fr` | Commandes unifiées pour les lecteurs web (expérimental) | Utilise les commandes partagées d’IPTVnator dans HTML5, Video.js et ArtPlayer. | +| `it` | Controlli unificati per i player web (sperimentale) | Usa i controlli condivisi di IPTVnator in HTML5, Video.js e ArtPlayer. | +| `pt` | Controlos unificados para leitores web (experimental) | Usa os controlos partilhados do IPTVnator em HTML5, Video.js e ArtPlayer. | +| `nl` | Uniforme bediening voor webspelers (experimenteel) | Gebruik de gedeelde IPTVnator-bediening in HTML5, Video.js en ArtPlayer. | +| `pl` | Ujednolicone sterowanie odtwarzaczami internetowymi (eksperymentalne) | Używaj wspólnych elementów sterowania IPTVnator w HTML5, Video.js i ArtPlayer. | +| `tr` | Web oynatıcıları için birleşik kontroller (deneysel) | HTML5, Video.js ve ArtPlayer'da IPTVnator'ın ortak kontrollerini kullan. | +| `el` | Ενοποιημένα στοιχεία ελέγχου για web players (πειραματικό) | Χρησιμοποιήστε τα κοινά στοιχεία ελέγχου του IPTVnator σε HTML5, Video.js και ArtPlayer. | +| `ja` | Webプレーヤーの統一コントロール(試験的) | HTML5、Video.js、ArtPlayerでIPTVnator共通のコントロールを使用します。 | +| `ko` | 웹 플레이어 통합 컨트롤(실험적) | HTML5, Video.js 및 ArtPlayer에서 IPTVnator 공통 컨트롤을 사용합니다. | +| `zh` | Web 播放器统一控件(实验性) | 在 HTML5、Video.js 和 ArtPlayer 中使用 IPTVnator 的共享控件。 | +| `zhtw` | Web 播放器統一控制項(實驗性) | 在 HTML5、Video.js 和 ArtPlayer 中使用 IPTVnator 的共用控制項。 | +| `ar` | عناصر تحكم موحّدة لمشغلات الويب (تجريبي) | استخدم عناصر التحكم المشتركة في IPTVnator مع HTML5 وVideo.js وArtPlayer. | +| `ary` | عناصر تحكم موحدة لمشغلات الويب (تجريبية) | استعمل عناصر التحكم المشتركة ديال IPTVnator مع HTML5 وVideo.js وArtPlayer. | +| `by` | Адзіныя элементы кіравання для вэб-прайгравальнікаў (эксперыментальна) | Выкарыстоўваць агульныя элементы кіравання IPTVnator у HTML5, Video.js і ArtPlayer. | + +- [ ] **Step 7: Validate locale parity and the full Settings project** + +Run: + +```bash +pnpm run i18n:check +pnpm nx test web --skip-nx-cache +pnpm nx lint web --skip-nx-cache +``` + +Expected: all commands PASS, with no missing or extra locale keys. + +- [ ] **Step 8: Commit the Settings UI** + +```bash +git add apps/web/src/app/settings/settings-form.utils.ts \ + apps/web/src/app/settings/settings-playback-section.component.ts \ + apps/web/src/app/settings/settings-playback-section.component.html \ + apps/web/src/app/settings/settings-playback-section.component.spec.ts \ + apps/web/src/app/settings/settings.component.spec.ts \ + apps/web/src/assets/i18n +git commit -m "feat(settings): expose shared web controls toggle" +``` + +## Task 3: Resolve an Immutable Controls Mode per Player Host + +**Files:** + +- Modify: `libs/ui/playback/src/lib/web-player-view/web-player-view.component.ts` +- Test: + `libs/ui/playback/src/lib/web-player-view/web-player-view.component.shared-controls.spec.ts` + +- [ ] **Step 1: Write a failing host-provider test** + +Update the Angular import in +`web-player-view.component.shared-controls.spec.ts`: + +```ts +import { Component, input, output, signal } from '@angular/core'; +``` + +Import the store and rollout token: + +```ts +import { RuntimeCapabilitiesService, SettingsStore } from '@iptvnator/services'; +import { WEB_PLAYER_SHARED_CONTROLS } from '../player-controls'; +``` + +Add a mutable mock signal beside the suite variables: + +```ts +const webPlayerSharedControls = signal(false); +``` + +Reset and provide it in `beforeEach()`: + +```ts +webPlayerSharedControls.set(false); + +{ + provide: SettingsStore, + useValue: { webPlayerSharedControls }, +}, +``` + +Insert the provider object into the existing TestBed `providers` array. + +Add this test: + +```ts +it('snapshots the saved shared-controls mode for each player host', () => { + webPlayerSharedControls.set(true); + fixture.detectChanges(); + + expect(fixture.debugElement.injector.get(WEB_PLAYER_SHARED_CONTROLS)).toBe( + true + ); + + webPlayerSharedControls.set(false); + expect(fixture.debugElement.injector.get(WEB_PLAYER_SHARED_CONTROLS)).toBe( + true + ); + + fixture.destroy(); + fixture = TestBed.createComponent(WebPlayerViewComponent); + component = fixture.componentInstance; + fixture.componentRef.setInput( + 'streamUrl', + 'https://example.com/new-session.ts' + ); + fixture.detectChanges(); + + expect(fixture.debugElement.injector.get(WEB_PLAYER_SHARED_CONTROLS)).toBe( + false + ); +}); +``` + +This proves both required properties: Save affects the next host, and an +existing host does not mix construction modes. + +- [ ] **Step 2: Run the host test and verify the red state** + +Run: + +```bash +pnpm nx test ui-playback --skip-nx-cache --runInBand \ + --testPathPatterns=web-player-view.component.shared-controls +``` + +Expected: FAIL because the root token still resolves the compile-time `false` +constant instead of the `SettingsStore` signal. + +- [ ] **Step 3: Add the component-scoped token provider** + +Update the services import in +`libs/ui/playback/src/lib/web-player-view/web-player-view.component.ts`: + +```ts +import { RuntimeCapabilitiesService, SettingsStore } from '@iptvnator/services'; +``` + +Import the rollout symbols: + +```ts +import { + WEB_PLAYER_SHARED_CONTROLS, + WEB_PLAYER_SHARED_CONTROLS_ENABLED, +} from '../player-controls'; +``` + +Add a focused factory before the component decorator: + +```ts +function resolveWebPlayerSharedControls(): boolean { + const storedValue = inject(SettingsStore).webPlayerSharedControls?.(); + + return typeof storedValue === 'boolean' + ? storedValue + : WEB_PLAYER_SHARED_CONTROLS_ENABLED; +} +``` + +Add the component-scoped provider: + +```ts +providers: [ + { + provide: WEB_PLAYER_SHARED_CONTROLS, + useFactory: resolveWebPlayerSharedControls, + }, +], +``` + +Insert this property into the existing `@Component` metadata. + +Angular creates and caches this primitive in the `WebPlayerViewComponent` +element injector. HTML5, Video.js, and ArtPlayer descendants receive one +immutable value for their shared construction-time branch. Embedded MPV does +not inject this token and therefore remains engine-driven. + +- [ ] **Step 4: Run focused and complete playback tests** + +Run: + +```bash +pnpm nx test ui-playback --skip-nx-cache --runInBand \ + --testPathPatterns='web-player-view.component|web-player-controls.flag' +pnpm nx test ui-playback --skip-nx-cache +pnpm nx lint ui-playback --skip-nx-cache +``` + +Expected: all commands PASS. Existing engine suites continue to prove that +flag-off preserves vendor controls and flag-on mounts exactly one shared UI. + +- [ ] **Step 5: Commit the playback wiring** + +```bash +git add libs/ui/playback/src/lib/web-player-view/web-player-view.component.ts \ + libs/ui/playback/src/lib/web-player-view/web-player-view.component.shared-controls.spec.ts +git commit -m "feat(playback): resolve shared controls from settings" +``` + +## Task 4: Prove Persistence and Next-Session Activation End to End + +**Files:** + +- Modify: `apps/web-e2e/src/settings.e2e.ts` +- Modify: `apps/electron-backend-e2e/src/settings.e2e.ts` + +- [ ] **Step 1: Add browser Save/reload persistence coverage** + +Add this test to `apps/web-e2e/src/settings.e2e.ts`: + +```ts +test('@settings @web Enable shared web player controls', async ({ page }) => { + await openSettings(page); + + const setting = page.locator( + '[data-test-id="web-player-shared-controls-setting"]' + ); + const checkbox = setting.locator('input[type="checkbox"]'); + + await expect(setting).toBeVisible(); + await expect(checkbox).not.toBeChecked(); + await checkbox.check(); + await saveSettings(page); + + await page.reload(); + await openSettings(page); + + await expect(checkbox).toBeChecked(); +}); +``` + +- [ ] **Step 2: Extend the Electron restart-persistence test** + +In the existing +`persists changed desktop settings across app restart` test, immediately after +selecting `html5`, enable the new checkbox: + +```ts +const sharedControlsCheckbox = firstLaunch.mainWindow + .getByTestId('web-player-shared-controls-setting') + .locator('input[type="checkbox"]'); +await expect(sharedControlsCheckbox).toBeVisible(); +await sharedControlsCheckbox.check(); +``` + +After the second launch confirms that HTML5 is selected, assert: + +```ts +await expect( + secondLaunch.mainWindow + .getByTestId('web-player-shared-controls-setting') + .locator('input[type="checkbox"]') +).toBeChecked(); +``` + +- [ ] **Step 3: Add a desktop next-session playback smoke test** + +Add this test to `apps/electron-backend-e2e/src/settings.e2e.ts`: + +```ts +test('@settings @playback @electron applies shared controls to the next HTML5 session', async ({ + dataDir, +}) => { + const app = await launchElectronApp(dataDir); + + try { + await openSettings(app.mainWindow); + await selectSettingsOption( + app.mainWindow, + 'select-video-player', + 'html5' + ); + await app.mainWindow + .getByTestId('web-player-shared-controls-setting') + .locator('input[type="checkbox"]') + .check(); + await saveSettings(app.mainWindow); + + await goToDashboard(app.mainWindow); + await importM3uPlaylistFromNativeDialog(app, m3uFixturePath); + await app.mainWindow.waitForURL(/\/workspace\/playlists\/.+/); + + const firstChannel = channelItemByTitle( + app.mainWindow, + 'Channel 1' + ).first(); + await expect(firstChannel).toBeVisible({ timeout: 20000 }); + await firstChannel.click(); + + await expect( + app.mainWindow.locator('app-html-video-player app-player-controls') + ).toBeVisible(); + await expect( + app.mainWindow.locator('app-html-video-player video[controls]') + ).toHaveCount(0); + } finally { + await closeElectronApp(app); + } +}); +``` + +- [ ] **Step 4: Run atomized web and Electron settings E2E** + +Run: + +```bash +pnpm nx run web-e2e:e2e-ci--src/settings.e2e.ts +pnpm nx run electron-backend-e2e:e2e-ci--src/settings.e2e.ts +``` + +Expected: both targets PASS. Browser settings survive reload, desktop settings +survive a full app restart, and the first HTML5 session after Save renders +`app-player-controls` without native video controls. + +- [ ] **Step 5: Commit E2E coverage** + +```bash +git add apps/web-e2e/src/settings.e2e.ts \ + apps/electron-backend-e2e/src/settings.e2e.ts +git commit -m "test(playback): cover shared controls setting" +``` + +## Task 5: Update Canonical Documentation and Run the Validation Ladder + +**Files:** + +- Modify: `README.md` +- Modify: `docs/architecture/player-controls-contract.md` +- Modify: `AGENTS.md` +- Modify: `CLAUDE.md` + +- [ ] **Step 1: Update the top-level Playback feature list** + +Add this bullet under `README.md` → `Features` → `Playback`: + +```md +- Optional unified IPTVnator controls for HTML5, Video.js, and ArtPlayer, enabled in **Settings → Playback** _(experimental)_ +``` + +- [ ] **Step 2: Update the canonical controls contract** + +In `docs/architecture/player-controls-contract.md`: + +1. Replace “a default-off web rollout token” in the current-status list with: + +```md +- a persisted, default-off web-player preference resolved through an immutable + per-host rollout token; +``` + +2. Replace the compile-time-only current-status paragraph with: + +```md +`Settings.webPlayerSharedControls` remains default-off. `WebPlayerViewComponent` +snapshots it into `WEB_PLAYER_SHARED_CONTROLS` when a new player host is +created, so HTML5, Video.js, and ArtPlayer switch atomically without an +application restart. Existing sessions never change controls mode in place. +``` + +3. Replace the rollout-symbol table rows with: + +```md +| Symbol / setting | Default | Current effect | +| ------------------------------------ | ---------------: | ----------------------------------------------------------------------- | +| `Settings.webPlayerSharedControls` | `false` | Persisted experimental opt-in shown for HTML5, Video.js, and ArtPlayer. | +| `WEB_PLAYER_SHARED_CONTROLS_ENABLED` | `false` | Default-off fallback for direct component use and focused tests. | +| `WEB_PLAYER_SHARED_CONTROLS` | session snapshot | Component-scoped immutable value consumed by the three web engines. | +``` + +4. State next to the Embedded MPV section: + +```md +The web-player preference does not affect Embedded MPV. Frame-copy always uses +the shared DOM controls, while native-view keeps its compositor-safe legacy +dock. +``` + +- [ ] **Step 3: Keep agent documentation synchronized** + +In both `AGENTS.md` and `CLAUDE.md`, update the shared-player-controls +description to include these exact facts: + +```md +- The persisted `Settings.webPlayerSharedControls` preference is default-off + and appears only when HTML5, Video.js, or ArtPlayer is selected. +- `WebPlayerViewComponent` snapshots the preference into the component-scoped + `WEB_PLAYER_SHARED_CONTROLS` token for each new host; an existing session + never changes controls mode in place. +- Embedded MPV ignores this preference: frame-copy always uses shared controls, + native-view retains the legacy dock, and external MPV/VLC own their UI. +``` + +Preserve the existing engine lifecycle details around these sentences. + +- [ ] **Step 4: Format and inspect documentation** + +Run: + +```bash +pnpm exec prettier --write README.md \ + docs/architecture/player-controls-contract.md AGENTS.md CLAUDE.md +git diff --check +``` + +Expected: Prettier completes successfully and `git diff --check` prints no +errors. + +- [ ] **Step 5: Run the complete regression ladder** + +Run: + +```bash +pnpm nx build shared-interfaces --skip-nx-cache +pnpm nx test services --skip-nx-cache +pnpm nx test web --skip-nx-cache +pnpm nx test ui-playback --skip-nx-cache +pnpm nx run-many --target=lint \ + --projects=shared-interfaces,services,web,ui-playback,web-e2e,electron-backend-e2e \ + --skip-nx-cache +pnpm run typecheck:web +pnpm run i18n:check +pnpm nx build web --configuration=production --skip-nx-cache +pnpm nx run web-e2e:e2e-ci--src/settings.e2e.ts +pnpm nx run electron-backend-e2e:e2e-ci--src/settings.e2e.ts +git diff --check +``` + +Expected: every command exits `0`. Record command results in the final handoff; +if a platform-dependent Electron E2E cannot run, report the exact environment +constraint and the strongest completed substitute instead of claiming it +passed. + +- [ ] **Step 6: Review the final diff against the approved design** + +Confirm all of the following before committing: + +- Default and malformed stored values resolve to `false`. +- Only HTML5, Video.js, and ArtPlayer expose the checkbox. +- Save affects the next player host without an app restart. +- One host receives one immutable boolean mode. +- Frame-copy Embedded MPV still always uses shared controls. +- Native-view Embedded MPV and external MPV/VLC remain unchanged. +- All 18 locales contain both keys. +- Unit, lint, typecheck, build, web E2E, and Electron E2E results are recorded. +- `README.md`, `docs/architecture/player-controls-contract.md`, `AGENTS.md`, + and `CLAUDE.md` describe the shipped behavior consistently. + +- [ ] **Step 7: Commit documentation and final integration** + +```bash +git add README.md docs/architecture/player-controls-contract.md \ + AGENTS.md CLAUDE.md +git commit -m "docs(playback): document shared controls preference" +```