mirror of
https://github.com/p-stream/p-stream.git
synced 2026-05-13 12:50:59 +00:00
add previous and next ep shortcuts
This commit is contained in:
parent
58594ae4b5
commit
869e4fca78
3 changed files with 235 additions and 2 deletions
|
|
@ -269,12 +269,15 @@
|
||||||
"syncSubtitlesLater": "Sync subtitles later (+0.5s)",
|
"syncSubtitlesLater": "Sync subtitles later (+0.5s)",
|
||||||
"barrelRoll": "Do a barrel roll! 🌀",
|
"barrelRoll": "Do a barrel roll! 🌀",
|
||||||
"closeOverlay": "Close overlay/modal",
|
"closeOverlay": "Close overlay/modal",
|
||||||
|
"nextEpisode": "Next episode",
|
||||||
|
"previousEpisode": "Previous episode",
|
||||||
"widescreenMode": "to toggle the widescreen button visibility",
|
"widescreenMode": "to toggle the widescreen button visibility",
|
||||||
"copyLinkWithTime": "+ click the title to copy the link with time"
|
"copyLinkWithTime": "+ click the title to copy the link with time"
|
||||||
},
|
},
|
||||||
"conditions": {
|
"conditions": {
|
||||||
"notInWatchParty": "Not in watch party",
|
"notInWatchParty": "Not in watch party",
|
||||||
"whenPaused": "When paused"
|
"whenPaused": "When paused",
|
||||||
|
"showsOnly": "Shows only"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,16 @@ const getShortcutGroups = (t: (key: string) => string): ShortcutGroup[] => [
|
||||||
description: t("global.keyboardShortcuts.shortcuts.skipBackward1"),
|
description: t("global.keyboardShortcuts.shortcuts.skipBackward1"),
|
||||||
condition: t("global.keyboardShortcuts.conditions.whenPaused"),
|
condition: t("global.keyboardShortcuts.conditions.whenPaused"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "N",
|
||||||
|
description: t("global.keyboardShortcuts.shortcuts.nextEpisode"),
|
||||||
|
condition: t("global.keyboardShortcuts.conditions.showsOnly"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "P",
|
||||||
|
description: t("global.keyboardShortcuts.shortcuts.previousEpisode"),
|
||||||
|
condition: t("global.keyboardShortcuts.conditions.showsOnly"),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
import { getMetaFromId } from "@/backend/metadata/getmeta";
|
||||||
|
import { MWMediaType } from "@/backend/metadata/types/mw";
|
||||||
import { useCaptions } from "@/components/player/hooks/useCaptions";
|
import { useCaptions } from "@/components/player/hooks/useCaptions";
|
||||||
|
import { usePlayerMeta } from "@/components/player/hooks/usePlayerMeta";
|
||||||
import { useVolume } from "@/components/player/hooks/useVolume";
|
import { useVolume } from "@/components/player/hooks/useVolume";
|
||||||
import { useOverlayRouter } from "@/hooks/useOverlayRouter";
|
import { useOverlayRouter } from "@/hooks/useOverlayRouter";
|
||||||
import { useOverlayStack } from "@/stores/interface/overlayStack";
|
import { useOverlayStack } from "@/stores/interface/overlayStack";
|
||||||
import { usePlayerStore } from "@/stores/player/store";
|
import { usePlayerStore } from "@/stores/player/store";
|
||||||
import { usePreferencesStore } from "@/stores/preferences";
|
import { usePreferencesStore } from "@/stores/preferences";
|
||||||
|
import { useProgressStore } from "@/stores/progress";
|
||||||
import { useSubtitleStore } from "@/stores/subtitles";
|
import { useSubtitleStore } from "@/stores/subtitles";
|
||||||
import { useEmpheralVolumeStore } from "@/stores/volume";
|
import { useEmpheralVolumeStore } from "@/stores/volume";
|
||||||
import { useWatchPartyStore } from "@/stores/watchParty";
|
import { useWatchPartyStore } from "@/stores/watchParty";
|
||||||
|
|
@ -20,6 +24,16 @@ export function KeyboardEvents() {
|
||||||
const duration = usePlayerStore((s) => s.progress.duration);
|
const duration = usePlayerStore((s) => s.progress.duration);
|
||||||
const { setVolume, toggleMute } = useVolume();
|
const { setVolume, toggleMute } = useVolume();
|
||||||
const isInWatchParty = useWatchPartyStore((s) => s.enabled);
|
const isInWatchParty = useWatchPartyStore((s) => s.enabled);
|
||||||
|
const meta = usePlayerStore((s) => s.meta);
|
||||||
|
const { setDirectMeta } = usePlayerMeta();
|
||||||
|
const setShouldStartFromBeginning = usePlayerStore(
|
||||||
|
(s) => s.setShouldStartFromBeginning,
|
||||||
|
);
|
||||||
|
const updateItem = useProgressStore((s) => s.updateItem);
|
||||||
|
const sourceId = usePlayerStore((s) => s.sourceId);
|
||||||
|
const setLastSuccessfulSource = usePreferencesStore(
|
||||||
|
(s) => s.setLastSuccessfulSource,
|
||||||
|
);
|
||||||
|
|
||||||
const { toggleLastUsed } = useCaptions();
|
const { toggleLastUsed } = useCaptions();
|
||||||
const setShowVolume = useEmpheralVolumeStore((s) => s.setShowVolume);
|
const setShowVolume = useEmpheralVolumeStore((s) => s.setShowVolume);
|
||||||
|
|
@ -47,6 +61,202 @@ export function KeyboardEvents() {
|
||||||
|
|
||||||
const setCurrentOverlay = useOverlayStack((s) => s.setCurrentOverlay);
|
const setCurrentOverlay = useOverlayStack((s) => s.setCurrentOverlay);
|
||||||
|
|
||||||
|
// Episode navigation functions
|
||||||
|
const navigateToNextEpisode = useCallback(async () => {
|
||||||
|
if (!meta || meta.type !== "show" || !meta.episode) return;
|
||||||
|
|
||||||
|
// Check if we're at the last episode of the current season
|
||||||
|
const isLastEpisode =
|
||||||
|
meta.episode.number === meta.episodes?.[meta.episodes.length - 1]?.number;
|
||||||
|
|
||||||
|
if (!isLastEpisode) {
|
||||||
|
// Navigate to next episode in current season
|
||||||
|
const nextEp = meta.episodes?.find(
|
||||||
|
(v) => v.number === meta.episode!.number + 1,
|
||||||
|
);
|
||||||
|
if (nextEp) {
|
||||||
|
if (sourceId) {
|
||||||
|
setLastSuccessfulSource(sourceId);
|
||||||
|
}
|
||||||
|
const metaCopy = { ...meta };
|
||||||
|
metaCopy.episode = nextEp;
|
||||||
|
setShouldStartFromBeginning(true);
|
||||||
|
setDirectMeta(metaCopy);
|
||||||
|
const defaultProgress = { duration: 0, watched: 0 };
|
||||||
|
updateItem({
|
||||||
|
meta: metaCopy,
|
||||||
|
progress: defaultProgress,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Navigate to first episode of next season
|
||||||
|
if (!meta.tmdbId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await getMetaFromId(MWMediaType.SERIES, meta.tmdbId);
|
||||||
|
if (data?.meta.type !== MWMediaType.SERIES) return;
|
||||||
|
|
||||||
|
const nextSeason = data.meta.seasons?.find(
|
||||||
|
(season) => season.number === (meta.season?.number ?? 0) + 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (nextSeason) {
|
||||||
|
const seasonData = await getMetaFromId(
|
||||||
|
MWMediaType.SERIES,
|
||||||
|
meta.tmdbId,
|
||||||
|
nextSeason.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (seasonData?.meta.type === MWMediaType.SERIES) {
|
||||||
|
const nextSeasonEpisodes = seasonData.meta.seasonData.episodes
|
||||||
|
.filter((episode) => {
|
||||||
|
// Simple aired check - episodes without air_date are considered aired
|
||||||
|
return (
|
||||||
|
!episode.air_date || new Date(episode.air_date) <= new Date()
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.map((episode) => ({
|
||||||
|
number: episode.number,
|
||||||
|
title: episode.title,
|
||||||
|
tmdbId: episode.id,
|
||||||
|
air_date: episode.air_date,
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (nextSeasonEpisodes.length > 0) {
|
||||||
|
const nextEp = nextSeasonEpisodes[0];
|
||||||
|
|
||||||
|
if (sourceId) {
|
||||||
|
setLastSuccessfulSource(sourceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const metaCopy = { ...meta };
|
||||||
|
metaCopy.episode = nextEp;
|
||||||
|
metaCopy.season = {
|
||||||
|
number: nextSeason.number,
|
||||||
|
title: nextSeason.title,
|
||||||
|
tmdbId: nextSeason.id,
|
||||||
|
};
|
||||||
|
metaCopy.episodes = nextSeasonEpisodes;
|
||||||
|
setShouldStartFromBeginning(true);
|
||||||
|
setDirectMeta(metaCopy);
|
||||||
|
const defaultProgress = { duration: 0, watched: 0 };
|
||||||
|
updateItem({
|
||||||
|
meta: metaCopy,
|
||||||
|
progress: defaultProgress,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load next season:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
meta,
|
||||||
|
setDirectMeta,
|
||||||
|
setShouldStartFromBeginning,
|
||||||
|
updateItem,
|
||||||
|
sourceId,
|
||||||
|
setLastSuccessfulSource,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const navigateToPreviousEpisode = useCallback(async () => {
|
||||||
|
if (!meta || meta.type !== "show" || !meta.episode) return;
|
||||||
|
|
||||||
|
// Check if we're at the first episode of the current season
|
||||||
|
const isFirstEpisode = meta.episode.number === meta.episodes?.[0]?.number;
|
||||||
|
|
||||||
|
if (!isFirstEpisode) {
|
||||||
|
// Navigate to previous episode in current season
|
||||||
|
const prevEp = meta.episodes?.find(
|
||||||
|
(v) => v.number === meta.episode!.number - 1,
|
||||||
|
);
|
||||||
|
if (prevEp) {
|
||||||
|
if (sourceId) {
|
||||||
|
setLastSuccessfulSource(sourceId);
|
||||||
|
}
|
||||||
|
const metaCopy = { ...meta };
|
||||||
|
metaCopy.episode = prevEp;
|
||||||
|
setShouldStartFromBeginning(true);
|
||||||
|
setDirectMeta(metaCopy);
|
||||||
|
const defaultProgress = { duration: 0, watched: 0 };
|
||||||
|
updateItem({
|
||||||
|
meta: metaCopy,
|
||||||
|
progress: defaultProgress,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Navigate to last episode of previous season
|
||||||
|
if (!meta.tmdbId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await getMetaFromId(MWMediaType.SERIES, meta.tmdbId);
|
||||||
|
if (data?.meta.type !== MWMediaType.SERIES) return;
|
||||||
|
|
||||||
|
const prevSeason = data.meta.seasons?.find(
|
||||||
|
(season) => season.number === (meta.season?.number ?? 0) - 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (prevSeason) {
|
||||||
|
const seasonData = await getMetaFromId(
|
||||||
|
MWMediaType.SERIES,
|
||||||
|
meta.tmdbId,
|
||||||
|
prevSeason.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (seasonData?.meta.type === MWMediaType.SERIES) {
|
||||||
|
const prevSeasonEpisodes = seasonData.meta.seasonData.episodes
|
||||||
|
.filter((episode) => {
|
||||||
|
// Simple aired check - episodes without air_date are considered aired
|
||||||
|
return (
|
||||||
|
!episode.air_date || new Date(episode.air_date) <= new Date()
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.map((episode) => ({
|
||||||
|
number: episode.number,
|
||||||
|
title: episode.title,
|
||||||
|
tmdbId: episode.id,
|
||||||
|
air_date: episode.air_date,
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (prevSeasonEpisodes.length > 0) {
|
||||||
|
const prevEp = prevSeasonEpisodes[prevSeasonEpisodes.length - 1];
|
||||||
|
|
||||||
|
if (sourceId) {
|
||||||
|
setLastSuccessfulSource(sourceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const metaCopy = { ...meta };
|
||||||
|
metaCopy.episode = prevEp;
|
||||||
|
metaCopy.season = {
|
||||||
|
number: prevSeason.number,
|
||||||
|
title: prevSeason.title,
|
||||||
|
tmdbId: prevSeason.id,
|
||||||
|
};
|
||||||
|
metaCopy.episodes = prevSeasonEpisodes;
|
||||||
|
setShouldStartFromBeginning(true);
|
||||||
|
setDirectMeta(metaCopy);
|
||||||
|
const defaultProgress = { duration: 0, watched: 0 };
|
||||||
|
updateItem({
|
||||||
|
meta: metaCopy,
|
||||||
|
progress: defaultProgress,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load previous season:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
meta,
|
||||||
|
setDirectMeta,
|
||||||
|
setShouldStartFromBeginning,
|
||||||
|
updateItem,
|
||||||
|
sourceId,
|
||||||
|
setLastSuccessfulSource,
|
||||||
|
]);
|
||||||
|
|
||||||
const dataRef = useRef({
|
const dataRef = useRef({
|
||||||
setShowVolume,
|
setShowVolume,
|
||||||
setVolume,
|
setVolume,
|
||||||
|
|
@ -74,6 +284,8 @@ export function KeyboardEvents() {
|
||||||
boostTimeoutRef,
|
boostTimeoutRef,
|
||||||
isPendingBoostRef,
|
isPendingBoostRef,
|
||||||
enableHoldToBoost,
|
enableHoldToBoost,
|
||||||
|
navigateToNextEpisode,
|
||||||
|
navigateToPreviousEpisode,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -104,6 +316,8 @@ export function KeyboardEvents() {
|
||||||
boostTimeoutRef,
|
boostTimeoutRef,
|
||||||
isPendingBoostRef,
|
isPendingBoostRef,
|
||||||
enableHoldToBoost,
|
enableHoldToBoost,
|
||||||
|
navigateToNextEpisode,
|
||||||
|
navigateToPreviousEpisode,
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
setShowVolume,
|
setShowVolume,
|
||||||
|
|
@ -127,6 +341,8 @@ export function KeyboardEvents() {
|
||||||
setSpeedBoosted,
|
setSpeedBoosted,
|
||||||
setShowSpeedIndicator,
|
setShowSpeedIndicator,
|
||||||
enableHoldToBoost,
|
enableHoldToBoost,
|
||||||
|
navigateToNextEpisode,
|
||||||
|
navigateToPreviousEpisode,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -304,6 +520,10 @@ export function KeyboardEvents() {
|
||||||
}
|
}
|
||||||
if (k === "Escape") dataRef.current.router.close();
|
if (k === "Escape") dataRef.current.router.close();
|
||||||
|
|
||||||
|
// Episode navigation (shows only)
|
||||||
|
if (keyL === "n") dataRef.current.navigateToNextEpisode();
|
||||||
|
if (keyL === "p") dataRef.current.navigateToPreviousEpisode();
|
||||||
|
|
||||||
// captions
|
// captions
|
||||||
if (keyL === "c") dataRef.current.toggleLastUsed().catch(() => {}); // ignore errors
|
if (keyL === "c") dataRef.current.toggleLastUsed().catch(() => {}); // ignore errors
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue