mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-13 20:56:19 -08:00
feat(dashboard): say where you left off instead of which provider it came from (#1646)
Every dashboard title carried a "Xtream · Series" / "Stalker · Movie" subtitle. Provider kind and content kind are the app's own taxonomy, not a property of the title, and they are identical on every card in a rail — so the one line that could tell two cards apart said nothing. The hero now shows the source name alone, through `playlistDisplayLabel` (a stored playlist name is routinely the pasted URL with credentials, or a MAC). Continue Watching cards show what actually varies: the "S1·E5" chip plus "12 min left". Favorites keep the title alone, Recently Added keeps the source name, and a meta row with nothing in it is no longer rendered. Stalker shows filed under Movies had no badge, no progress and no resume. An embedded-VOD row announces its episodes through a `series[]` array and carries no `is_series` flag, so `extractStalkerItemType` reports `movie` on purpose — the item must keep routing to the VOD catalog — while its progress is a set of episode positions keyed by the parent id, which the dashboard was looking up as a single `vod` row and never finding. Split the two questions: `PortalActivityItem.watch_kind` records the progress model when it differs from the routing type, and every reader that has to choose goes through `resolvePortalActivityWatchKind` instead of `type`. That makes the resume handoff reachable for Stalker, so wire it through `STALKER_SERIES_RESUME_TARGET`: consumed once after the series positions are read, hydrating a lazy Ministra season first with a bounded two-attempt retry, and playing nothing at all when the position read failed rather than restarting the episode from zero. Also fixes the global-recent route template, which bound `[seriesResume]` only on its Xtream branch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
954c8ad65e
commit
4e575a810e
26 files changed
+1514
-160
No files matched your search
@@ -17,9 +17,22 @@ class MockXtreamCollectionDetailComponent {
|
||||
readonly closeRequested = output<void>();
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-stalker-collection-detail',
|
||||
template: '',
|
||||
})
|
||||
class MockStalkerCollectionDetailComponent {
|
||||
readonly item = input<UnifiedCollectionItem | null>(null);
|
||||
readonly seriesResume = input<SeriesResumeTarget | null>(null);
|
||||
readonly closeRequested = output<void>();
|
||||
}
|
||||
|
||||
jest.unstable_mockModule('@iptvnator/portal/xtream/feature', () => ({
|
||||
XtreamCollectionDetailComponent: MockXtreamCollectionDetailComponent,
|
||||
}));
|
||||
jest.unstable_mockModule('@iptvnator/portal/stalker/feature', () => ({
|
||||
StalkerCollectionDetailComponent: MockStalkerCollectionDetailComponent,
|
||||
}));
|
||||
|
||||
@Component({
|
||||
imports: [GlobalCollectionDetailHostComponent],
|
||||
@@ -123,6 +136,38 @@ describe('GlobalCollectionDetailHostComponent', () => {
|
||||
expect(instance.seriesResume()).toEqual(seriesResume);
|
||||
});
|
||||
|
||||
it('creates the Stalker detail with the series resume target attached', async () => {
|
||||
// A Stalker embedded-VOD show routes as a movie; the resume target
|
||||
// still has to reach the detail so the series view can consume it.
|
||||
const seriesResume = {
|
||||
seriesXtreamId: 17672,
|
||||
contentXtreamId: 1879880442,
|
||||
seasonNumber: 2,
|
||||
episodeNumber: 6,
|
||||
};
|
||||
fixture.componentInstance.seriesResume.set(seriesResume);
|
||||
fixture.componentInstance.item.set({
|
||||
uid: 'stalker::stalker-1::17672',
|
||||
name: 'Old School',
|
||||
contentType: 'movie',
|
||||
sourceType: 'stalker',
|
||||
playlistId: 'stalker-1',
|
||||
playlistName: 'Portal',
|
||||
stalkerId: '17672',
|
||||
categoryId: '7',
|
||||
});
|
||||
await stabilize();
|
||||
|
||||
const detail = fixture.debugElement.query(
|
||||
By.directive(MockStalkerCollectionDetailComponent)
|
||||
);
|
||||
expect(detail).not.toBeNull();
|
||||
const instance =
|
||||
detail?.componentInstance as MockStalkerCollectionDetailComponent;
|
||||
expect(instance.item()?.stalkerId).toBe('17672');
|
||||
expect(instance.seriesResume()).toEqual(seriesResume);
|
||||
});
|
||||
|
||||
it('re-emits close requests and clears the detail with the item', async () => {
|
||||
fixture.componentInstance.item.set(xtreamSeriesItem);
|
||||
await stabilize();
|
||||
|
||||
@@ -113,9 +113,9 @@ export class GlobalCollectionDetailHostComponent implements OnDestroy {
|
||||
environmentInjector: this.environmentInjector,
|
||||
});
|
||||
componentRef.setInput('item', item);
|
||||
if (item.sourceType === 'xtream') {
|
||||
componentRef.setInput('seriesResume', seriesResume);
|
||||
}
|
||||
// Both portal details accept the one-shot resume handoff (Xtream
|
||||
// series, Stalker embedded-VOD / lazy is_series shows).
|
||||
componentRef.setInput('seriesResume', seriesResume);
|
||||
this.subscribeToClose(componentRef.instance);
|
||||
}
|
||||
|
||||
@@ -196,6 +196,7 @@ export class GlobalCollectionDetailHostComponent implements OnDestroy {
|
||||
} @else if (item.sourceType === 'stalker') {
|
||||
<app-global-collection-detail-host
|
||||
[item]="item"
|
||||
[seriesResume]="seriesResume"
|
||||
(closeRequested)="close()"
|
||||
/>
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user