mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 01:56:16 -08:00
docs(downloads): plan detail navigation follow-up
This commit is contained in:
1 parent
211000a8e3
commit
5ef5d6713f
1 file changed
+460
@@ -0,0 +1,460 @@
|
||||
# Download Manager Detail Navigation 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:** Make completed download cards honor the global cover-size preference, open existing detail pages from navigable card content, and prefer local playback on downloaded movie details.
|
||||
|
||||
**Architecture:** Keep `DownloadLibraryComponent` presentational by emitting one generic detail intent and one explicit local-file action. Reuse the existing navigation and download action services in `DownloadsComponent`. In Xtream and shared Stalker VOD details, preserve external-player Stop/Opening precedence, then choose the completed local file before the unchanged provider playback path; provider playback remains a neutral secondary action. No metadata, schema, IPC, or offline-routing layer is added.
|
||||
|
||||
**Tech Stack:** Angular 21.2 standalone components, signal inputs/outputs and computed state, Angular Material, ngx-translate, Jest 30, Nx 22.7, SCSS shared content-grid tokens, Electron Playwright E2E.
|
||||
|
||||
---
|
||||
|
||||
## File map
|
||||
|
||||
- `libs/portal/downloads/feature/src/lib/download-library.component.{ts,html,scss,spec.ts}` — card intent, accessibility, and global grid tokens.
|
||||
- `libs/portal/downloads/feature/src/lib/downloads.component.{html,scss,spec.ts}` — smart-container wiring and skeleton grid tokens.
|
||||
- `libs/portal/xtream/feature/src/lib/vod-details/vod-details-route.component.{ts,html}` — Xtream offline-primary/provider-secondary behavior.
|
||||
- `libs/portal/xtream/feature/src/lib/vod-details/vod-details-route.actions.spec.ts` — rendered Xtream download actions.
|
||||
- `libs/portal/xtream/feature/src/lib/vod-details/vod-details-route-playback.spec.ts` and `vod-details-route.harness.ts` — external-session and provider-playback regression coverage.
|
||||
- `libs/ui/playback/src/lib/vod-details/vod-details.component.{ts,html,spec.ts}` — shared Stalker VOD action behavior and tests.
|
||||
- `apps/electron-backend-e2e/src/downloads.e2e.ts` — exact movie detail navigation and Small/Medium/Large geometry.
|
||||
- `docs/architecture/download-manager.md` — canonical interaction, sizing, and no-cache boundary.
|
||||
- `.changes/downloads-manager-mvp.md` — user-facing corrected behavior.
|
||||
|
||||
## Task 1: Fix completed-card intent and sizing with TDD
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `libs/portal/downloads/feature/src/lib/download-library.component.spec.ts`
|
||||
- Modify: `libs/portal/downloads/feature/src/lib/download-library.component.ts`
|
||||
- Modify: `libs/portal/downloads/feature/src/lib/download-library.component.html`
|
||||
- Modify: `libs/portal/downloads/feature/src/lib/download-library.component.scss`
|
||||
|
||||
- [ ] **Step 1: Write the failing interaction tests**
|
||||
|
||||
Add a generic detail output and pin the movie/local split:
|
||||
|
||||
```ts
|
||||
it('opens movie details from artwork and title without playing locally', async () => {
|
||||
const card = byTestId('download-library-movie-9');
|
||||
const opened: DownloadItem[] = [];
|
||||
const actions: unknown[] = [];
|
||||
component.openRequested.subscribe((selected) => opened.push(selected));
|
||||
component.itemAction.subscribe((action) => actions.push(action));
|
||||
|
||||
await click(button(card, 'Open details: Moonrise artwork'));
|
||||
await click(button(card, 'Open details: Moonrise'));
|
||||
|
||||
expect(opened).toEqual([MOVIE, MOVIE]);
|
||||
expect(actions).toEqual([]);
|
||||
});
|
||||
```
|
||||
|
||||
Update the grouped-series test to subscribe to `openRequested`. Extend the
|
||||
legacy episode test to click `.download-library__artwork-button` and
|
||||
`.download-library__title-button`, assert two concrete `play` actions, and
|
||||
assert no detail output. Keep the existing toolbar Play assertion so it proves
|
||||
that explicit Play remains local.
|
||||
|
||||
- [ ] **Step 2: Write the failing style contract**
|
||||
|
||||
Read `download-library.component.scss` in the spec and assert the grid block
|
||||
contains both global inputs and no fixed mobile columns:
|
||||
|
||||
```ts
|
||||
expect(styles).toContain('var(--cover-grid-min-width, 148px)');
|
||||
expect(styles).toContain('var(--cover-gap, 16px)');
|
||||
expect(styles).not.toContain(
|
||||
'grid-template-columns: repeat(2, minmax(0, 1fr))'
|
||||
);
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run the focused spec and verify RED**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm nx test portal-downloads-feature --runInBand \
|
||||
--testPathPatterns=download-library.component.spec.ts
|
||||
```
|
||||
|
||||
Expected: FAIL because `openRequested` and movie detail controls do not exist,
|
||||
and the SCSS still contains fixed `168px`, `clamp(...)`, and two-column values.
|
||||
|
||||
- [ ] **Step 4: Implement the generic detail intent**
|
||||
|
||||
Replace the series-only output with:
|
||||
|
||||
```ts
|
||||
readonly openRequested = output<DownloadItem>();
|
||||
|
||||
protected canOpen(item: DownloadItem): boolean {
|
||||
return this.availablePlaylistIds().has(item.playlistId);
|
||||
}
|
||||
|
||||
protected openDetails(item: DownloadItem): void {
|
||||
if (!this.isPending(item) && this.canOpen(item)) {
|
||||
this.openRequested.emit(item);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Grouped-series artwork/title call
|
||||
`openDetails(entity.representative)`. Movie artwork/title call
|
||||
`openDetails(entity.item)` and use translated `Open details` accessible names.
|
||||
The explicit movie toolbar Play continues to call
|
||||
`emitAction('play', entity.item)`. Ungrouped episode artwork/title continue to
|
||||
call `emitAction('play', entity.item)`.
|
||||
|
||||
- [ ] **Step 5: Consume the existing cover tokens**
|
||||
|
||||
Use the shared mixin without feature-owned sizing:
|
||||
|
||||
```scss
|
||||
.download-library__grid {
|
||||
@include grid.content-grid(
|
||||
$min-width: min(100%, var(--cover-grid-min-width, 148px)),
|
||||
$gap: var(--cover-gap, 16px)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Remove the `repeat(2, ...)` mobile override. Keep the existing card visuals and
|
||||
overflow-safe `min(100%, ...)`.
|
||||
|
||||
- [ ] **Step 6: Run the focused spec and verify GREEN**
|
||||
|
||||
Run the command from Step 3. Expected: PASS.
|
||||
|
||||
- [ ] **Step 7: Commit the card change**
|
||||
|
||||
```bash
|
||||
git add libs/portal/downloads/feature/src/lib/download-library.component.*
|
||||
git commit -m "fix(downloads): open completed movies in details"
|
||||
```
|
||||
|
||||
## Task 2: Wire movie navigation and synchronize the skeleton
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `libs/portal/downloads/feature/src/lib/downloads.component.spec.ts`
|
||||
- Modify: `libs/portal/downloads/feature/src/lib/downloads.component.html`
|
||||
- Modify: `libs/portal/downloads/feature/src/lib/downloads.component.scss`
|
||||
|
||||
- [ ] **Step 1: Write the failing container regression**
|
||||
|
||||
Render one completed movie, click its artwork, and prove navigation is distinct
|
||||
from the toolbar Play:
|
||||
|
||||
```ts
|
||||
downloads.set([download(16, { title: 'Movie details' })]);
|
||||
await fixture.whenStable();
|
||||
const card = fixture.nativeElement.querySelector(
|
||||
'[data-test-id="download-library-movie-16"]'
|
||||
) as HTMLElement;
|
||||
|
||||
(card.querySelector('.download-library__artwork-button') as HTMLButtonElement)
|
||||
.click();
|
||||
await fixture.whenStable();
|
||||
expect(navigation.open).toHaveBeenCalledWith(downloads()[0]);
|
||||
expect(downloadsService.playDownload).not.toHaveBeenCalled();
|
||||
|
||||
navigation.open.mockClear();
|
||||
(card.querySelector('.download-library__actions button') as HTMLButtonElement)
|
||||
.click();
|
||||
await fixture.whenStable();
|
||||
expect(downloadsService.playDownload).toHaveBeenCalledWith('/downloads/16.mp4');
|
||||
expect(navigation.open).not.toHaveBeenCalled();
|
||||
```
|
||||
|
||||
Read `downloads.component.scss` and assert `.downloads__skeleton-cards` uses
|
||||
`--cover-grid-min-width` and `--cover-gap`.
|
||||
|
||||
- [ ] **Step 2: Run the container spec and verify RED**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm nx test portal-downloads-feature --runInBand \
|
||||
--testPathPatterns=downloads.component.spec.ts
|
||||
```
|
||||
|
||||
Expected: FAIL because movie detail output is not bound and skeleton sizing is
|
||||
fixed at `150px`.
|
||||
|
||||
- [ ] **Step 3: Wire the output and tokens**
|
||||
|
||||
Bind the presentational intent:
|
||||
|
||||
```html
|
||||
<app-download-library
|
||||
[entities]="model().library"
|
||||
[availablePlaylistIds]="availablePlaylistIds()"
|
||||
[pendingIds]="pendingIds()"
|
||||
(itemAction)="runAction($event)"
|
||||
(openRequested)="openInLibrary($event)"
|
||||
(episodesOpened)="openDownloadedSeries($event)"
|
||||
/>
|
||||
```
|
||||
|
||||
Use the same token contract for the skeleton:
|
||||
|
||||
```scss
|
||||
.downloads__skeleton-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(
|
||||
auto-fill,
|
||||
minmax(min(100%, var(--cover-grid-min-width, 148px)), 1fr)
|
||||
);
|
||||
gap: var(--cover-gap, 16px);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run both Download Manager specs and verify GREEN**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm nx test portal-downloads-feature --runInBand \
|
||||
--testPathPatterns=download-library.component.spec.ts \
|
||||
--testPathPatterns=downloads.component.spec.ts
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Commit the integration**
|
||||
|
||||
```bash
|
||||
git add libs/portal/downloads/feature/src/lib/downloads.component.*
|
||||
git commit -m "fix(downloads): honor the global cover size"
|
||||
```
|
||||
|
||||
## Task 3: Prefer offline playback in movie details with TDD
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `libs/ui/playback/src/lib/vod-details/vod-details.component.spec.ts`
|
||||
- Modify: `libs/ui/playback/src/lib/vod-details/vod-details.component.ts`
|
||||
- Modify: `libs/ui/playback/src/lib/vod-details/vod-details.component.html`
|
||||
- Modify: `libs/portal/xtream/feature/src/lib/vod-details/vod-details-route.actions.spec.ts`
|
||||
- Modify: `libs/portal/xtream/feature/src/lib/vod-details/vod-details-route-playback.spec.ts`
|
||||
- Modify: `libs/portal/xtream/feature/src/lib/vod-details/vod-details-route.harness.ts`
|
||||
- Modify: `libs/portal/xtream/feature/src/lib/vod-details/vod-details-route.component.ts`
|
||||
- Modify: `libs/portal/xtream/feature/src/lib/vod-details/vod-details-route.component.html`
|
||||
|
||||
- [ ] **Step 1: Write failing shared-detail tests**
|
||||
|
||||
For a downloaded Stalker VOD, assert:
|
||||
|
||||
```ts
|
||||
expect(host.textContent).toContain('Offline');
|
||||
await click(primaryButton);
|
||||
expect(playDownload).toHaveBeenCalledWith('/downloads/movie.mp4');
|
||||
expect(playClicked).not.toHaveBeenCalled();
|
||||
|
||||
await click(sourceButton);
|
||||
expect(playClicked).toHaveBeenCalledWith(STALKER_VOD);
|
||||
```
|
||||
|
||||
Set a matching external MPV session and assert the primary button says
|
||||
`Stop MPV`, calls `closeSession`, and does not call `playDownload`.
|
||||
|
||||
- [ ] **Step 2: Write failing Xtream detail tests**
|
||||
|
||||
Drive the existing sparse VOD fixture with a completed path. Assert the rendered
|
||||
detail includes `DOWNLOADS.OFFLINE`, primary click calls
|
||||
`playDownload('/downloads/catalog-movie.mp4')` without constructing provider
|
||||
playback, and the `PORTALS.MULTI_SOURCE.PLAY_FROM_SOURCE` button starts the
|
||||
existing provider path. Add a playback-spec case proving a matching MPV/VLC
|
||||
Stop action still outranks local playback.
|
||||
|
||||
- [ ] **Step 3: Run both affected project tests and verify RED**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node tools/testing/run-web-esm-lib-tests.mjs \
|
||||
libs/ui/playback/src/lib/vod-details --runInBand
|
||||
pnpm nx test portal-xtream-feature --runInBand \
|
||||
--testPathPatterns=vod-details-route.actions.spec.ts \
|
||||
--testPathPatterns=vod-details-route-playback.spec.ts
|
||||
```
|
||||
|
||||
Expected: FAIL because the primary action still starts provider playback and
|
||||
the completed button is still the local secondary action.
|
||||
|
||||
- [ ] **Step 4: Implement shared Stalker precedence**
|
||||
|
||||
Keep Stop first, then local, then provider:
|
||||
|
||||
```ts
|
||||
async onPrimaryAction(): Promise<void> {
|
||||
if (this.isExternalStopAction()) {
|
||||
await this.stopExternalPlayback();
|
||||
return;
|
||||
}
|
||||
if (this.isDownloaded()) {
|
||||
await this.playFromLocal();
|
||||
return;
|
||||
}
|
||||
this.onProviderAction();
|
||||
}
|
||||
|
||||
onProviderAction(): void {
|
||||
if (this.hasPlaybackPosition()) {
|
||||
this.onResume();
|
||||
return;
|
||||
}
|
||||
this.onPlay();
|
||||
}
|
||||
```
|
||||
|
||||
In the template, external Opening/Stop label and icon remain first. Otherwise a
|
||||
downloaded item uses `DOWNLOADS.PLAY_LOCAL` with `play_circle`. Add an
|
||||
`DOWNLOADS.OFFLINE` detail tag. Replace the duplicate completed local button
|
||||
with a neutral `PORTALS.MULTI_SOURCE.PLAY_FROM_SOURCE` button calling
|
||||
`onProviderAction()`, visible only while the external button state is idle.
|
||||
|
||||
- [ ] **Step 5: Implement Xtream precedence**
|
||||
|
||||
Extract today's provider/pinned/resume body into
|
||||
`playFromProviderSource(vodItem)`. Keep `onPrimaryAction` as:
|
||||
|
||||
```ts
|
||||
async onPrimaryAction(vodItem: XtreamVodDetails | null): Promise<void> {
|
||||
if (this.playback.isExternalStopAction()) {
|
||||
this.playback.onPrimaryAction(vodItem);
|
||||
return;
|
||||
}
|
||||
if (this.isDownloaded()) {
|
||||
await this.playFromLocal();
|
||||
return;
|
||||
}
|
||||
await this.playFromProviderSource(vodItem);
|
||||
}
|
||||
```
|
||||
|
||||
The secondary source button calls `playFromProviderSource(playableItem)`.
|
||||
External labels/icons remain first, downloaded idle state renders
|
||||
`DOWNLOADS.PLAY_LOCAL`, and both rich/fallback detail tags render
|
||||
`DOWNLOADS.OFFLINE`. Hide the source button while an owned external session is
|
||||
launching or running.
|
||||
|
||||
- [ ] **Step 6: Run the tests and verify GREEN**
|
||||
|
||||
Run both commands from Step 3. Expected: PASS.
|
||||
|
||||
- [ ] **Step 7: Run the complete affected unit suites**
|
||||
|
||||
```bash
|
||||
pnpm nx test portal-downloads-feature --runInBand
|
||||
pnpm nx test portal-xtream-feature --runInBand
|
||||
pnpm nx test ui-playback --runInBand
|
||||
pnpm nx test portal-stalker-feature --runInBand
|
||||
```
|
||||
|
||||
Expected: PASS with no stale detail-action assertions.
|
||||
|
||||
- [ ] **Step 8: Commit the detail behavior**
|
||||
|
||||
```bash
|
||||
git add libs/portal/xtream/feature/src/lib/vod-details \
|
||||
libs/ui/playback/src/lib/vod-details
|
||||
git commit -m "fix(downloads): prefer offline movie playback"
|
||||
```
|
||||
|
||||
## Task 4: Acceptance coverage, docs, and release note
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `apps/electron-backend-e2e/src/downloads.e2e.ts`
|
||||
- Modify: `docs/architecture/download-manager.md`
|
||||
- Modify: `.changes/downloads-manager-mvp.md`
|
||||
|
||||
- [ ] **Step 1: Add the failing Electron acceptance assertions**
|
||||
|
||||
Seed one completed movie using a real imported VOD `xtream_id` and
|
||||
`category_id`. Assert its artwork/title opens:
|
||||
|
||||
```ts
|
||||
`/workspace/xtreams/${playlistId}/vod/${categoryId}/${xtreamId}`
|
||||
```
|
||||
|
||||
On the Downloads page, set `document.documentElement.dataset.coverSize` to
|
||||
`small`, `medium`, and `large`; record a completed card width, computed
|
||||
`columnGap`, and grid column count. Assert the three settings yield the global
|
||||
12/16/20px gaps and materially different card geometry.
|
||||
|
||||
- [ ] **Step 2: Run targeted E2E and verify RED**
|
||||
|
||||
```bash
|
||||
pnpm nx run electron-backend-e2e:e2e-ci--src/downloads.e2e.ts
|
||||
```
|
||||
|
||||
Expected: FAIL because movie artwork plays locally and all three cover settings
|
||||
produce the same grid.
|
||||
|
||||
- [ ] **Step 3: Run targeted E2E and verify GREEN after Tasks 1–3**
|
||||
|
||||
Run the command from Step 2. Expected: PASS.
|
||||
|
||||
- [ ] **Step 4: Update canonical documentation**
|
||||
|
||||
Document:
|
||||
|
||||
- movie/grouped-series artwork/title to provider details;
|
||||
- explicit card Play and legacy episode fallback to the local file;
|
||||
- primary local and secondary provider playback on downloaded VOD details;
|
||||
- external-player Stop precedence;
|
||||
- global `--cover-grid-min-width` / `--cover-gap` ownership;
|
||||
- provider-backed details and metadata caching as a future boundary.
|
||||
|
||||
- [ ] **Step 5: Update and validate the release note**
|
||||
|
||||
Keep the existing single `.changes/downloads-manager-mvp.md` note and describe
|
||||
the corrected navigation, offline playback, and cover sizing in at most 400
|
||||
characters.
|
||||
|
||||
```bash
|
||||
pnpm run release:notes:validate
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 6: Run repository validation**
|
||||
|
||||
```bash
|
||||
pnpm nx lint portal-downloads-feature
|
||||
pnpm nx lint portal-xtream-feature
|
||||
pnpm nx lint ui-playback
|
||||
pnpm nx lint portal-stalker-feature
|
||||
pnpm nx run web:typecheck
|
||||
pnpm nx build web
|
||||
pnpm run i18n:check
|
||||
pnpm run release:notes:validate
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Expected: every command exits zero.
|
||||
|
||||
- [ ] **Step 7: Rebuild and inspect Electron**
|
||||
|
||||
Rebuild the Electron/web artifacts, restart only the worktree test instance,
|
||||
and inspect:
|
||||
|
||||
- Small/Medium/Large at wide and narrow widths;
|
||||
- movie card artwork/title versus explicit Play;
|
||||
- downloaded Xtream and Stalker VOD action hierarchy;
|
||||
- light and dark themes;
|
||||
- absence of renderer console errors.
|
||||
|
||||
- [ ] **Step 8: Commit and push**
|
||||
|
||||
```bash
|
||||
git add apps/electron-backend-e2e/src/downloads.e2e.ts \
|
||||
docs/architecture/download-manager.md \
|
||||
.changes/downloads-manager-mvp.md
|
||||
git commit -m "test(downloads): cover detail-first offline flow"
|
||||
git push origin agent/download-manager-mvp
|
||||
```
|
||||
Reference in new issue
Block a user