mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 18:36:15 -08:00
refactor(playback): share series navigation state
This commit is contained in:
1 parent
4e30289293
commit
e03dfc9c2a
4 files changed
+131
-122
No files matched your search
+13
-57
@@ -46,12 +46,12 @@ import {
|
||||
StalkerVodSource,
|
||||
} from '@iptvnator/portal/stalker/data-access';
|
||||
import {
|
||||
formatSeriesEpisodeLabel,
|
||||
getSeriesEpisodeMetadata,
|
||||
getSeriesPlaybackNavigation,
|
||||
type PlaybackFallbackRequest,
|
||||
PortalInlinePlayerComponent,
|
||||
type SeriesEpisodeMetadata,
|
||||
resolveSeriesPlaybackEpisodeState,
|
||||
type SeriesPlaybackEpisodeState,
|
||||
type SeriesPlaybackNavigation,
|
||||
} from '@iptvnator/ui/playback';
|
||||
import {
|
||||
DownloadsService,
|
||||
@@ -280,35 +280,12 @@ export class StalkerSeriesViewComponent implements OnDestroy {
|
||||
computed<SeriesPlaybackEpisodeState<XtreamSerieEpisode> | null>(() =>
|
||||
this.getInlineEpisodeState()
|
||||
);
|
||||
readonly inlineEpisodeMetadata = computed<SeriesEpisodeMetadata | null>(() => {
|
||||
const state = this.inlineEpisodeState();
|
||||
if (!state) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
label: formatSeriesEpisodeLabel(
|
||||
state.seasonNumber,
|
||||
state.episodeNumber
|
||||
),
|
||||
seasonNumber: state.seasonNumber,
|
||||
episodeNumber: state.episodeNumber,
|
||||
title: state.episode.title,
|
||||
};
|
||||
});
|
||||
readonly inlineSeriesNavigation =
|
||||
computed<SeriesPlaybackNavigation | null>(() => {
|
||||
const state = this.inlineEpisodeState();
|
||||
if (!state) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
canPrevious: Boolean(state.previous),
|
||||
canNext: Boolean(state.next),
|
||||
autoplayEnabled: true,
|
||||
};
|
||||
});
|
||||
readonly inlineEpisodeMetadata = computed(() =>
|
||||
getSeriesEpisodeMetadata(this.inlineEpisodeState())
|
||||
);
|
||||
readonly inlineSeriesNavigation = computed(() =>
|
||||
getSeriesPlaybackNavigation(this.inlineEpisodeState())
|
||||
);
|
||||
|
||||
/**
|
||||
* Handles season selection from the container.
|
||||
@@ -628,31 +605,10 @@ export class StalkerSeriesViewComponent implements OnDestroy {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const [seasonKey, episodes] of Object.entries(this.mappedSeasons())) {
|
||||
const episodeIndex = episodes.findIndex(
|
||||
(episode) => Number(episode.id) === Number(currentEpisodeId)
|
||||
);
|
||||
if (episodeIndex < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const episode = episodes[episodeIndex];
|
||||
const seasonNumber =
|
||||
Number(episode.season) || Number(seasonKey) || 0;
|
||||
const episodeNumber =
|
||||
Number(episode.episode_num) || episodeIndex + 1;
|
||||
|
||||
return {
|
||||
seasonKey,
|
||||
seasonNumber,
|
||||
episodeNumber,
|
||||
episode,
|
||||
previous: episodes[episodeIndex - 1] ?? null,
|
||||
next: episodes[episodeIndex + 1] ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
return resolveSeriesPlaybackEpisodeState({
|
||||
episodesBySeason: this.mappedSeasons(),
|
||||
currentEpisodeId,
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
|
||||
@@ -26,12 +26,12 @@ import {
|
||||
} from '@iptvnator/portal/shared/util';
|
||||
import { XtreamStore } from '@iptvnator/portal/xtream/data-access';
|
||||
import {
|
||||
formatSeriesEpisodeLabel,
|
||||
getSeriesEpisodeMetadata,
|
||||
getSeriesPlaybackNavigation,
|
||||
type PlaybackFallbackRequest,
|
||||
PortalInlinePlayerComponent,
|
||||
type SeriesEpisodeMetadata,
|
||||
resolveSeriesPlaybackEpisodeState,
|
||||
type SeriesPlaybackEpisodeState,
|
||||
type SeriesPlaybackNavigation,
|
||||
} from '@iptvnator/ui/playback';
|
||||
import { PlaybackPositionRuntimeBridgeService } from '@iptvnator/services';
|
||||
import {
|
||||
@@ -118,35 +118,12 @@ export class SerialDetailsComponent implements OnInit, OnDestroy {
|
||||
computed<SeriesPlaybackEpisodeState<XtreamSerieEpisode> | null>(() =>
|
||||
this.getInlineEpisodeState()
|
||||
);
|
||||
readonly inlineEpisodeMetadata = computed<SeriesEpisodeMetadata | null>(() => {
|
||||
const state = this.inlineEpisodeState();
|
||||
if (!state) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
label: formatSeriesEpisodeLabel(
|
||||
state.seasonNumber,
|
||||
state.episodeNumber
|
||||
),
|
||||
seasonNumber: state.seasonNumber,
|
||||
episodeNumber: state.episodeNumber,
|
||||
title: state.episode.title,
|
||||
};
|
||||
});
|
||||
readonly inlineSeriesNavigation =
|
||||
computed<SeriesPlaybackNavigation | null>(() => {
|
||||
const state = this.inlineEpisodeState();
|
||||
if (!state) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
canPrevious: Boolean(state.previous),
|
||||
canNext: Boolean(state.next),
|
||||
autoplayEnabled: true,
|
||||
};
|
||||
});
|
||||
readonly inlineEpisodeMetadata = computed(() =>
|
||||
getSeriesEpisodeMetadata(this.inlineEpisodeState())
|
||||
);
|
||||
readonly inlineSeriesNavigation = computed(() =>
|
||||
getSeriesPlaybackNavigation(this.inlineEpisodeState())
|
||||
);
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
@@ -458,36 +435,12 @@ export class SerialDetailsComponent implements OnInit, OnDestroy {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const [seasonKey, episodes] of Object.entries(episodesBySeason)) {
|
||||
const episodeIndex = episodes.findIndex(
|
||||
(episode) => Number(episode.id) === Number(currentEpisodeId)
|
||||
);
|
||||
if (episodeIndex < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const episode = episodes[episodeIndex];
|
||||
const seasonNumber =
|
||||
Number(episode.season) ||
|
||||
Number(seasonKey) ||
|
||||
playback.contentInfo.seasonNumber ||
|
||||
0;
|
||||
const episodeNumber =
|
||||
Number(episode.episode_num) ||
|
||||
playback.contentInfo.episodeNumber ||
|
||||
episodeIndex + 1;
|
||||
|
||||
return {
|
||||
seasonKey,
|
||||
seasonNumber,
|
||||
episodeNumber,
|
||||
episode,
|
||||
previous: episodes[episodeIndex - 1] ?? null,
|
||||
next: episodes[episodeIndex + 1] ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
return resolveSeriesPlaybackEpisodeState({
|
||||
episodesBySeason,
|
||||
currentEpisodeId,
|
||||
fallbackSeasonNumber: playback.contentInfo.seasonNumber,
|
||||
fallbackEpisodeNumber: playback.contentInfo.episodeNumber,
|
||||
});
|
||||
}
|
||||
|
||||
async handlePlaybackToggleRequested(
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
effect,
|
||||
inject,
|
||||
input,
|
||||
output,
|
||||
signal,
|
||||
untracked,
|
||||
viewChild,
|
||||
@@ -76,9 +77,9 @@ export class EmbeddedMpvPlayerComponent implements OnDestroy {
|
||||
currentTime: number;
|
||||
duration: number;
|
||||
}>();
|
||||
@Output() playbackEnded = new EventEmitter<void>();
|
||||
@Output() previousEpisodeRequested = new EventEmitter<void>();
|
||||
@Output() nextEpisodeRequested = new EventEmitter<void>();
|
||||
readonly playbackEnded = output<void>();
|
||||
readonly previousEpisodeRequested = output<void>();
|
||||
readonly nextEpisodeRequested = output<void>();
|
||||
|
||||
private readonly overlayVisibility = inject(
|
||||
EmbeddedMpvOverlayVisibilityService
|
||||
|
||||
@@ -20,6 +20,22 @@ export interface SeriesPlaybackEpisodeState<TEpisode> {
|
||||
next: TEpisode | null;
|
||||
}
|
||||
|
||||
export interface SeriesPlaybackEpisodeLike {
|
||||
id: string | number;
|
||||
season?: string | number;
|
||||
episode_num?: string | number;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export interface ResolveSeriesPlaybackEpisodeStateOptions<
|
||||
TEpisode extends SeriesPlaybackEpisodeLike,
|
||||
> {
|
||||
episodesBySeason: Record<string, readonly TEpisode[]> | null | undefined;
|
||||
currentEpisodeId: string | number | null | undefined;
|
||||
fallbackSeasonNumber?: number;
|
||||
fallbackEpisodeNumber?: number;
|
||||
}
|
||||
|
||||
export function formatSeriesEpisodeLabel(
|
||||
seasonNumber: number,
|
||||
episodeNumber: number
|
||||
@@ -27,6 +43,89 @@ export function formatSeriesEpisodeLabel(
|
||||
return `S${padSeriesNumber(seasonNumber)}E${padSeriesNumber(episodeNumber)}`;
|
||||
}
|
||||
|
||||
export function resolveSeriesPlaybackEpisodeState<
|
||||
TEpisode extends SeriesPlaybackEpisodeLike,
|
||||
>({
|
||||
episodesBySeason,
|
||||
currentEpisodeId,
|
||||
fallbackSeasonNumber,
|
||||
fallbackEpisodeNumber,
|
||||
}: ResolveSeriesPlaybackEpisodeStateOptions<TEpisode>): SeriesPlaybackEpisodeState<TEpisode> | null {
|
||||
if (
|
||||
!episodesBySeason ||
|
||||
currentEpisodeId === null ||
|
||||
currentEpisodeId === undefined
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const [seasonKey, episodes] of Object.entries(episodesBySeason)) {
|
||||
const episodeIndex = episodes.findIndex(
|
||||
(episode) => Number(episode.id) === Number(currentEpisodeId)
|
||||
);
|
||||
if (episodeIndex < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const episode = episodes[episodeIndex];
|
||||
const seasonNumber =
|
||||
Number(episode.season) ||
|
||||
Number(seasonKey) ||
|
||||
fallbackSeasonNumber ||
|
||||
0;
|
||||
const episodeNumber =
|
||||
Number(episode.episode_num) ||
|
||||
fallbackEpisodeNumber ||
|
||||
episodeIndex + 1;
|
||||
|
||||
return {
|
||||
seasonKey,
|
||||
seasonNumber,
|
||||
episodeNumber,
|
||||
episode,
|
||||
previous: episodes[episodeIndex - 1] ?? null,
|
||||
next: episodes[episodeIndex + 1] ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getSeriesEpisodeMetadata<
|
||||
TEpisode extends SeriesPlaybackEpisodeLike,
|
||||
>(
|
||||
state: SeriesPlaybackEpisodeState<TEpisode> | null
|
||||
): SeriesEpisodeMetadata | null {
|
||||
if (!state) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
label: formatSeriesEpisodeLabel(
|
||||
state.seasonNumber,
|
||||
state.episodeNumber
|
||||
),
|
||||
seasonNumber: state.seasonNumber,
|
||||
episodeNumber: state.episodeNumber,
|
||||
title: state.episode.title,
|
||||
};
|
||||
}
|
||||
|
||||
export function getSeriesPlaybackNavigation(
|
||||
state: SeriesPlaybackEpisodeState<unknown> | null,
|
||||
autoplayEnabled = true
|
||||
): SeriesPlaybackNavigation | null {
|
||||
if (!state) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
canPrevious: Boolean(state.previous),
|
||||
canNext: Boolean(state.next),
|
||||
autoplayEnabled,
|
||||
};
|
||||
}
|
||||
|
||||
function padSeriesNumber(value: number): string {
|
||||
const normalized = Number.isFinite(value) && value > 0 ? value : 0;
|
||||
return String(normalized).padStart(2, '0');
|
||||
|
||||
Reference in new issue
Block a user