mirror of
https://github.com/p-stream/p-stream.git
synced 2026-03-11 09:45:33 +00:00
init shuffle season, add button
not working
This commit is contained in:
parent
752e8cadb1
commit
77e20ec349
7 changed files with 158 additions and 45 deletions
|
|
@ -86,6 +86,7 @@ export enum Icons {
|
|||
TRANSLATE = "translate",
|
||||
THUMBS_UP = "thumbsUp",
|
||||
THUMBS_DOWN = "thumbsDown",
|
||||
SHUFFLE = "shuffle",
|
||||
}
|
||||
|
||||
export interface IconProps {
|
||||
|
|
@ -189,6 +190,7 @@ const iconList: Record<Icons, string> = {
|
|||
translate: `<svg width="1em" height="1em" fill="currentColor" viewBox="0 0 52 52" data-name="Layer 1" id="Layer_1" xmlns="http://www.w3.org/2000/svg"><path d="M39,18.67H35.42l-4.2,11.12A29,29,0,0,1,20.6,24.91a28.76,28.76,0,0,0,7.11-14.49h5.21a2,2,0,0,0,0-4H19.67V2a2,2,0,1,0-4,0V6.42H2.41a2,2,0,0,0,0,4H7.63a28.73,28.73,0,0,0,7.1,14.49A29.51,29.51,0,0,1,3.27,30a2,2,0,0,0,.43,4,1.61,1.61,0,0,0,.44-.05,32.56,32.56,0,0,0,13.53-6.25,32,32,0,0,0,12.13,5.9L22.83,52H28l2.7-7.76H43.64L46.37,52h5.22Zm-15.3-8.25a23.76,23.76,0,0,1-6,11.86,23.71,23.71,0,0,1-6-11.86Zm8.68,29.15,4.83-13.83L42,39.57Z"/></svg>`,
|
||||
thumbsUp: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 640 640"><!--!Font Awesome Free v7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M144 224C161.7 224 176 238.3 176 256L176 512C176 529.7 161.7 544 144 544L96 544C78.3 544 64 529.7 64 512L64 256C64 238.3 78.3 224 96 224L144 224zM334.6 80C361.9 80 384 102.1 384 129.4L384 133.6C384 140.4 382.7 147.2 380.2 153.5L352 224L512 224C538.5 224 560 245.5 560 272C560 291.7 548.1 308.6 531.1 316C548.1 323.4 560 340.3 560 360C560 383.4 543.2 402.9 521 407.1C525.4 414.4 528 422.9 528 432C528 454.2 513 472.8 492.6 478.3C494.8 483.8 496 489.8 496 496C496 522.5 474.5 544 448 544L360.1 544C323.8 544 288.5 531.6 260.2 508.9L248 499.2C232.8 487.1 224 468.7 224 449.2L224 262.6C224 247.7 227.5 233 234.1 219.7L290.3 107.3C298.7 90.6 315.8 80 334.6 80z"/></svg>`,
|
||||
thumbsDown: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 640 640"><!--!Font Awesome Free v7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M448 96C474.5 96 496 117.5 496 144C496 150.3 494.7 156.2 492.6 161.7C513 167.2 528 185.8 528 208C528 217.1 525.4 225.6 521 232.9C543.2 237.1 560 256.6 560 280C560 299.7 548.1 316.6 531.1 324C548.1 331.4 560 348.3 560 368C560 394.5 538.5 416 512 416L352 416L380.2 486.4C382.7 492.7 384 499.5 384 506.3L384 510.5C384 537.8 361.9 559.9 334.6 559.9C315.9 559.9 298.8 549.3 290.4 532.6L234.1 420.3C227.4 407 224 392.3 224 377.4L224 190.8C224 171.4 232.9 153 248 140.8L260.2 131.1C288.6 108.4 323.8 96 360.1 96L448 96zM144 160C161.7 160 176 174.3 176 192L176 448C176 465.7 161.7 480 144 480L96 480C78.3 480 64 465.7 64 448L64 192C64 174.3 78.3 160 96 160L144 160z"/></svg>`,
|
||||
shuffle: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-shuffle"><polyline points="16 3 21 3 21 8"></polyline><line x1="4" y1="20" x2="21" y2="3"></line><polyline points="21 16 21 21 16 21"></polyline><line x1="15" y1="15" x2="21" y2="21"></line><line x1="4" y1="4" x2="9" y2="9"></line></svg>`,
|
||||
};
|
||||
|
||||
export const Icon = memo((props: IconProps) => {
|
||||
|
|
|
|||
|
|
@ -3,10 +3,12 @@ import { t } from "i18next";
|
|||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import { MWMediaType } from "@/backend/metadata/types/mw";
|
||||
import { Button } from "@/components/buttons/Button";
|
||||
import { Dropdown } from "@/components/form/Dropdown";
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
import { Modal, ModalCard, useModal } from "@/components/overlays/Modal";
|
||||
import { usePlayerMeta } from "@/components/player/hooks/usePlayerMeta";
|
||||
import { hasAired } from "@/components/player/utils/aired";
|
||||
import { useBookmarkStore } from "@/stores/bookmarks";
|
||||
import { getProgressPercentage, useProgressStore } from "@/stores/progress";
|
||||
|
|
@ -47,6 +49,7 @@ export function EpisodeCarousel({
|
|||
}>({});
|
||||
const updateItem = useProgressStore((s) => s.updateItem);
|
||||
const confirmModal = useModal("season-watch-confirm");
|
||||
const { setPlayerMeta, setDirectMeta } = usePlayerMeta();
|
||||
|
||||
const handleScroll = (direction: "left" | "right") => {
|
||||
if (!carouselRef.current) return;
|
||||
|
|
@ -239,6 +242,102 @@ export function EpisodeCarousel({
|
|||
confirmModal.show();
|
||||
};
|
||||
|
||||
const handleShuffle = () => {
|
||||
console.log("handleShuffle called", {
|
||||
mediaId,
|
||||
selectedSeason,
|
||||
showFavorites,
|
||||
});
|
||||
|
||||
if (!mediaId) {
|
||||
console.log("No mediaId, returning early");
|
||||
return;
|
||||
}
|
||||
|
||||
const episodesToShuffle = showFavorites
|
||||
? favoriteEpisodes
|
||||
: episodes.filter((ep) => ep.season_number === selectedSeason);
|
||||
|
||||
console.log("Episodes to shuffle:", episodesToShuffle.length);
|
||||
|
||||
if (episodesToShuffle.length === 0) {
|
||||
console.log("No episodes to shuffle, returning early");
|
||||
return;
|
||||
}
|
||||
|
||||
// Fisher-Yates shuffle
|
||||
const shuffled = [...episodesToShuffle];
|
||||
for (let i = shuffled.length - 1; i > 0; i -= 1) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
||||
}
|
||||
|
||||
const firstEp = shuffled[0];
|
||||
console.log("First shuffled episode:", firstEp);
|
||||
|
||||
// Find the season for the first episode
|
||||
const seasonData = seasons.find(
|
||||
(s) => s.season_number === firstEp.season_number,
|
||||
);
|
||||
if (!seasonData) {
|
||||
console.log("No seasonData found, returning early");
|
||||
return;
|
||||
}
|
||||
console.log("Season data:", seasonData);
|
||||
|
||||
// Create meta with shuffled episodes
|
||||
const meta = setPlayerMeta(
|
||||
{
|
||||
meta: {
|
||||
id: mediaId.toString(),
|
||||
type: MWMediaType.SERIES,
|
||||
title: mediaTitle || "",
|
||||
poster: mediaPosterUrl,
|
||||
year: new Date().getFullYear().toString(),
|
||||
seasons: seasons.map((s) => ({
|
||||
id: s.id.toString(),
|
||||
number: s.season_number,
|
||||
title: s.name,
|
||||
})),
|
||||
seasonData: {
|
||||
id: seasonData.id.toString(),
|
||||
number: firstEp.season_number,
|
||||
title: "",
|
||||
episodes: shuffled.map((ep) => ({
|
||||
id: ep.id.toString(),
|
||||
number: ep.episode_number,
|
||||
title: ep.name,
|
||||
air_date: ep.air_date,
|
||||
still_path: ep.still_path,
|
||||
overview: ep.overview,
|
||||
})),
|
||||
},
|
||||
},
|
||||
tmdbId: mediaId.toString(),
|
||||
},
|
||||
firstEp.id.toString(),
|
||||
);
|
||||
|
||||
console.log("Meta created:", meta);
|
||||
console.log(
|
||||
"Episodes in meta:",
|
||||
meta?.episodes?.map((e) => `${e.number}: ${e.title}`),
|
||||
);
|
||||
|
||||
if (meta) {
|
||||
// Create a copy of meta with shuffled property
|
||||
const metaWithShuffled = { ...meta, shuffled: true };
|
||||
setDirectMeta(metaWithShuffled);
|
||||
|
||||
// Navigate to the first shuffled episode
|
||||
const url = `/media/tmdb-tv-${mediaId}-${(mediaTitle || "").toLowerCase().replace(/[^a-z0-9]+/g, "-")}/${seasonData.id}/${firstEp.id}`;
|
||||
console.log("Navigating to:", url);
|
||||
window.location.href = url;
|
||||
} else {
|
||||
console.log("Meta creation failed");
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
confirmModal.hide();
|
||||
};
|
||||
|
|
@ -505,6 +604,14 @@ export function EpisodeCarousel({
|
|||
|
||||
{/* Season Watched Confirmation */}
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleShuffle}
|
||||
className="p-1.5 bg-dropdown-background hover:bg-dropdown-hoverBackground transition-colors rounded-full"
|
||||
title={t("player.menus.episodes.shuffle")}
|
||||
>
|
||||
<Icon icon={Icons.SHUFFLE} className="h-5 w-5 text-white" />
|
||||
</button>
|
||||
<Modal id={confirmModal.id}>
|
||||
<ModalCard>
|
||||
<h3 className="text-lg font-semibold text-white mb-4">
|
||||
|
|
|
|||
|
|
@ -120,10 +120,12 @@ export function NextEpisodeButton(props: {
|
|||
const updateItem = useProgressStore((s) => s.updateItem);
|
||||
const sourceId = usePlayerStore((s) => s.sourceId);
|
||||
|
||||
const currentEpIndex =
|
||||
meta?.episodes?.findIndex((v) => v.tmdbId === meta.episode?.tmdbId) ?? -1;
|
||||
const isLastEpisode =
|
||||
!meta?.episode?.number || !meta?.episodes?.at(-1)?.number
|
||||
currentEpIndex === -1
|
||||
? false
|
||||
: meta.episode.number === meta.episodes.at(-1)!.number;
|
||||
: currentEpIndex === (meta?.episodes?.length ?? 0) - 1;
|
||||
|
||||
const seasons = useSeasons(meta?.tmdbId, isLastEpisode);
|
||||
|
||||
|
|
@ -148,13 +150,20 @@ export function NextEpisodeButton(props: {
|
|||
|
||||
const nextEp = isLastEpisode
|
||||
? nextSeasonEpisode.value
|
||||
: meta?.episodes?.find(
|
||||
(v) => v.number === (meta?.episode?.number ?? 0) + 1,
|
||||
);
|
||||
: meta?.episodes?.[currentEpIndex + 1];
|
||||
|
||||
const loadNextEpisode = useCallback(() => {
|
||||
if (!meta || !nextEp) return;
|
||||
|
||||
console.log("loadNextEpisode called");
|
||||
console.log("meta.shuffled:", meta.shuffled);
|
||||
console.log(
|
||||
"Current episodes order:",
|
||||
meta.episodes?.map((e) => `${e.number}: ${e.title}`),
|
||||
);
|
||||
console.log("Current episode:", meta.episode?.number, meta.episode?.title);
|
||||
console.log("Next episode:", nextEp?.number, nextEp?.title);
|
||||
|
||||
// Store the current source as the last successful source
|
||||
if (sourceId) {
|
||||
setLastSuccessfulSource(sourceId);
|
||||
|
|
|
|||
|
|
@ -92,15 +92,28 @@ export function KeyboardEvents() {
|
|||
const navigateToNextEpisode = useCallback(async () => {
|
||||
if (!meta || meta.type !== "show" || !meta.episode) return;
|
||||
|
||||
console.log("navigateToNextEpisode called");
|
||||
console.log("meta.shuffled:", meta.shuffled);
|
||||
console.log(
|
||||
"Current episodes order:",
|
||||
meta.episodes?.map((e) => `${e.number}: ${e.title}`),
|
||||
);
|
||||
|
||||
// Check if we're at the last episode of the current season
|
||||
const isLastEpisode =
|
||||
meta.episode.number === meta.episodes?.[meta.episodes.length - 1]?.number;
|
||||
const currentEpIndex =
|
||||
meta.episodes?.findIndex((v) => v.tmdbId === meta.episode?.tmdbId) ?? -1;
|
||||
const isLastEpisode = currentEpIndex === (meta.episodes?.length ?? 0) - 1;
|
||||
|
||||
console.log("Current episode:", meta.episode?.number, meta.episode?.title);
|
||||
console.log("Current ep index:", currentEpIndex);
|
||||
console.log("Is last episode:", isLastEpisode);
|
||||
|
||||
if (!isLastEpisode) {
|
||||
// Navigate to next episode in current season
|
||||
const nextEp = meta.episodes?.find(
|
||||
(v) => v.number === meta.episode!.number + 1,
|
||||
);
|
||||
const nextEp = meta.episodes?.[currentEpIndex + 1];
|
||||
|
||||
console.log("Next episode:", nextEp?.number, nextEp?.title);
|
||||
|
||||
if (nextEp) {
|
||||
if (sourceId) {
|
||||
setLastSuccessfulSource(sourceId);
|
||||
|
|
|
|||
|
|
@ -20,9 +20,10 @@ export function MediaSession() {
|
|||
|
||||
const changeEpisode = useCallback(
|
||||
(change: number) => {
|
||||
const nextEp = meta?.episodes?.find(
|
||||
(v) => v.number === (meta?.episode?.number ?? 0) + change,
|
||||
);
|
||||
const currentEpIndex =
|
||||
meta?.episodes?.findIndex((v) => v.tmdbId === meta.episode?.tmdbId) ??
|
||||
-1;
|
||||
const nextEp = meta?.episodes?.[currentEpIndex + change];
|
||||
|
||||
if (!meta || !nextEp) return;
|
||||
const metaCopy = { ...meta };
|
||||
|
|
@ -178,7 +179,11 @@ export function MediaSession() {
|
|||
updatePositionState(e.seekTime);
|
||||
});
|
||||
|
||||
if ((meta?.episode?.number ?? 1) > 1) {
|
||||
const currentEpIndex =
|
||||
meta?.episodes?.findIndex((v) => v.tmdbId === meta.episode?.tmdbId) ?? -1;
|
||||
const totalEpisodes = meta?.episodes?.length ?? 0;
|
||||
|
||||
if (currentEpIndex > 0) {
|
||||
navigator.mediaSession.setActionHandler("previoustrack", () =>
|
||||
changeEpisode(-1),
|
||||
);
|
||||
|
|
@ -186,9 +191,7 @@ export function MediaSession() {
|
|||
navigator.mediaSession.setActionHandler("previoustrack", null);
|
||||
}
|
||||
|
||||
const totalEpisodes = meta?.episodes?.length ?? 0;
|
||||
const currentEpisodeNumber = meta?.episode?.number ?? 0;
|
||||
if (currentEpisodeNumber > 0 && currentEpisodeNumber < totalEpisodes) {
|
||||
if (currentEpIndex >= 0 && currentEpIndex < totalEpisodes - 1) {
|
||||
navigator.mediaSession.setActionHandler("nexttrack", () =>
|
||||
changeEpisode(1),
|
||||
);
|
||||
|
|
@ -210,6 +213,8 @@ export function MediaSession() {
|
|||
meta?.type,
|
||||
meta?.poster,
|
||||
meta?.season?.number,
|
||||
meta?.episode?.tmdbId,
|
||||
meta?.episodes,
|
||||
]);
|
||||
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -106,41 +106,17 @@ export function BookmarkSyncer() {
|
|||
syncTimeout = setTimeout(syncImmediately, 100);
|
||||
};
|
||||
|
||||
// Override the addBookmark function to trigger immediate sync
|
||||
const originalAddBookmark = useBookmarkStore.getState().addBookmark;
|
||||
useBookmarkStore.setState({
|
||||
addBookmark: (...args) => {
|
||||
originalAddBookmark(...args);
|
||||
// Trigger debounced sync after adding bookmark
|
||||
const unsub = useBookmarkStore.subscribe((state, prevState) => {
|
||||
if (state.updateQueue.length > prevState.updateQueue.length) {
|
||||
debouncedSync();
|
||||
},
|
||||
});
|
||||
|
||||
// Override removeBookmark to trigger immediate sync
|
||||
const originalRemoveBookmark = useBookmarkStore.getState().removeBookmark;
|
||||
useBookmarkStore.setState({
|
||||
removeBookmark: (...args) => {
|
||||
originalRemoveBookmark(...args);
|
||||
// Trigger debounced sync after removing bookmark
|
||||
debouncedSync();
|
||||
},
|
||||
});
|
||||
|
||||
// Override toggleFavoriteEpisode to trigger immediate sync
|
||||
const originalToggleFavoriteEpisode =
|
||||
useBookmarkStore.getState().toggleFavoriteEpisode;
|
||||
useBookmarkStore.setState({
|
||||
toggleFavoriteEpisode: (...args) => {
|
||||
originalToggleFavoriteEpisode(...args);
|
||||
// Trigger debounced sync after toggling favorite episode
|
||||
debouncedSync();
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (syncTimeout) {
|
||||
clearTimeout(syncTimeout);
|
||||
}
|
||||
unsub();
|
||||
};
|
||||
}, [removeUpdateItem, url]);
|
||||
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ export interface PlayerMeta {
|
|||
tmdbId: string;
|
||||
title: string;
|
||||
};
|
||||
shuffled?: boolean;
|
||||
}
|
||||
|
||||
export interface Caption {
|
||||
|
|
|
|||
Loading…
Reference in a new issue