Feat: Added whole season watched button

This commit is contained in:
SimSalabimse 2025-07-28 15:31:29 +02:00 committed by Pas
parent 65ea4c5091
commit cbf1d678f2
2 changed files with 181 additions and 12 deletions

View file

@ -0,0 +1,57 @@
import { Button } from "@/components/buttons/Button";
import {
OverlayDisplay,
OverlayPortal,
} from "@/components/overlays/OverlayDisplay";
interface ConfirmOverlayProps {
isOpen: boolean;
message: string;
onConfirm: (event: React.MouseEvent) => void;
onCancel: () => void;
confirmButtonTheme?: "white" | "purple" | "secondary" | "danger" | "glass";
cancelButtonTheme?: "white" | "purple" | "secondary" | "danger" | "glass";
backdropOpacity?: number;
backdropColor?: string;
}
export function ConfirmOverlay({
isOpen,
message,
onConfirm,
onCancel,
confirmButtonTheme = "purple",
cancelButtonTheme = "secondary",
backdropOpacity = 0.5,
backdropColor = "black",
}: ConfirmOverlayProps) {
return (
<OverlayPortal show={isOpen}>
<div
className={`fixed inset-0 bg-${backdropColor} bg-opacity-${backdropOpacity * 100} flex items-center justify-center z-50`}
>
<OverlayDisplay>
<div className="bg-background-main text-white p-4 rounded-lg shadow-md flex flex-col items-center pointer-events-auto gap-3">
<p className="mb-4, text-center">{message}</p>
<div className="flex space-x-2">
<Button
theme={confirmButtonTheme}
onClick={onConfirm}
padding="px-3 py-1"
>
Confirm
</Button>
<Button
theme={cancelButtonTheme}
onClick={onCancel}
padding="px-3 py-1"
>
Cancel
</Button>
</div>
</div>
</OverlayDisplay>
</div>
</OverlayPortal>
);
}

View file

@ -6,6 +6,7 @@ import { Link } from "react-router-dom";
import { Button } from "@/components/buttons/Button";
import { Dropdown } from "@/components/form/Dropdown";
import { Icon, Icons } from "@/components/Icon";
import { ConfirmOverlay } from "@/components/overlays/details/ConfirmOverlay";
import { hasAired } from "@/components/player/utils/aired";
import { useProgressStore } from "@/stores/progress";
@ -25,6 +26,8 @@ export function EpisodeCarousel({
const [showEpisodeMenu, setShowEpisodeMenu] = useState(false);
const [customSeason, setCustomSeason] = useState("");
const [customEpisode, setCustomEpisode] = useState("");
const [SeasonWatched, setSeasonWatched] = useState(false);
const [isConfirmOpen, setIsConfirmOpen] = useState(false);
const [expandedEpisodes, setExpandedEpisodes] = useState<{
[key: number]: boolean;
}>({});
@ -203,10 +206,66 @@ export function EpisodeCarousel({
}
};
// Toggle whole season watch status
const toggleSeasonWatchStatus = (event: React.MouseEvent) => {
event.preventDefault();
event.stopPropagation();
setIsConfirmOpen(true);
};
const handleCancel = () => {
setIsConfirmOpen(false);
};
const currentSeasonEpisodes = episodes.filter(
(ep) => ep.season_number === selectedSeason,
);
const handleConfirm = (event: React.MouseEvent) => {
try {
const episodeWatchedStatus: boolean[] = [];
currentSeasonEpisodes.forEach((episode: any) => {
const episodeProgress =
progress[mediaId?.toString() ?? ""]?.episodes?.[episode.id];
const percentage = episodeProgress
? (episodeProgress.progress.watched /
episodeProgress.progress.duration) *
100
: 0;
const isAired = hasAired(episode.air_date);
const isWatched = percentage > 90;
if (isAired && !isWatched) {
episodeWatchedStatus.push(isWatched);
}
});
const hasUnwatched = episodeWatchedStatus.length >= 1;
currentSeasonEpisodes.forEach((episode: any) => {
const episodeProgress =
progress[mediaId?.toString() ?? ""]?.episodes?.[episode.id];
const percentage = episodeProgress
? (episodeProgress.progress.watched /
episodeProgress.progress.duration) *
100
: 0;
const isAired = hasAired(episode.air_date);
const isWatched = percentage > 90;
if (hasUnwatched && isAired && !isWatched) {
toggleWatchStatus(episode.id, event); // Mark unwatched as watched
} else if (!hasUnwatched && isAired && isWatched) {
toggleWatchStatus(episode.id, event); // Mark watched as unwatched
}
});
setIsConfirmOpen(false);
} catch (error) {
console.error("Error in handleConfirm:", error);
setIsConfirmOpen(false);
}
};
const toggleEpisodeExpansion = (
episodeId: number,
event: React.MouseEvent,
@ -259,6 +318,34 @@ export function EpisodeCarousel({
};
}, [episodes, expandedEpisodes]);
useEffect(() => {
const episodeWatchedStatus: boolean[] = [];
currentSeasonEpisodes.forEach((episode: any) => {
const episodeProgress =
progress[mediaId?.toString() ?? ""]?.episodes?.[episode.id];
const percentage = episodeProgress
? (episodeProgress.progress.watched /
episodeProgress.progress.duration) *
100
: 0;
const isAired = hasAired(episode.air_date);
const isWatched = percentage > 90;
if (isAired && !isWatched) {
episodeWatchedStatus.push(isWatched);
}
});
let toggle: boolean;
if (episodeWatchedStatus.length >= 1) {
setSeasonWatched(true); // If no episodes are watched, we want to mark all as watched
} else {
setSeasonWatched(false); // if all episodes are watched, we want to mark all as unwatched
}
}, [currentSeasonEpisodes, episodes, mediaId, progress]);
return (
<div className="mt-6 md:mt-0">
{/* Season Selector */}
@ -323,17 +410,43 @@ export function EpisodeCarousel({
)}
</div>
</div>
<Dropdown
options={seasons.map((season) => ({
id: season.season_number.toString(),
name: `${t("details.season")} ${season.season_number}`,
}))}
selectedItem={{
id: selectedSeason.toString(),
name: `${t("details.season")} ${selectedSeason}`,
}}
setSelectedItem={(item) => onSeasonChange(Number(item.id))}
/>
<div className="flex items-center justify-between gap-2">
{isConfirmOpen && (
<ConfirmOverlay
isOpen={isConfirmOpen}
message={
SeasonWatched
? "Are you sure you want to mark the season as watched?"
: "Are you sure you want to mark the season as unwatched?"
}
onConfirm={handleConfirm}
onCancel={handleCancel}
/>
)}
<button
type="button"
onClick={(e) => toggleSeasonWatchStatus(e)}
className="p-1.5 bg-black/50 rounded-full hover:bg-black/80 transition-colors"
title={t("Mark season as watched")}
>
<Icon
icon={SeasonWatched ? Icons.EYE : Icons.EYE_SLASH}
className="h-5 w-5 text-white"
/>
</button>
<Dropdown
options={seasons.map((season) => ({
id: season.season_number.toString(),
name: `${t("details.season")} ${season.season_number}`,
}))}
selectedItem={{
id: selectedSeason.toString(),
name: `${t("details.season")} ${selectedSeason}`,
}}
setSelectedItem={(item) => onSeasonChange(Number(item.id))}
/>
</div>
</div>
{/* Episodes Carousel */}
@ -359,7 +472,6 @@ export function EpisodeCarousel({
>
{/* Add padding before the first card */}
<div className="flex-shrink-0 w-4" />
{currentSeasonEpisodes.map((episode) => {
const isActive =
showProgress?.episode?.id === episode.id.toString();